feat: legacy mobile apps created

This commit is contained in:
Achintha Isuru
2025-12-02 23:51:04 -05:00
parent 850441ca64
commit 8e7753b324
1519 changed files with 0 additions and 16 deletions

View File

@@ -0,0 +1,104 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_input.dart';
import 'package:krow/features/shifts/presentation/dialogs/cancel_dialog/cancel_reason_dropdown.dart';
class CancelDialogView extends StatefulWidget {
const CancelDialogView({super.key});
@override
State<CancelDialogView> createState() => _CancelDialogViewState();
}
class _CancelDialogViewState extends State<CancelDialogView> {
String selectedReason = '';
final _textEditingController = TextEditingController();
@override
Widget build(BuildContext context) {
return Center(
child: AnimatedSize(
duration: const Duration(milliseconds: 300),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
decoration: BoxDecoration(
color: AppColors.grayWhite,
borderRadius: BorderRadius.circular(24),
),
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 56),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'cancel_shift'.tr(),
style: AppTextStyles.headingH1.copyWith(height: 1),
textAlign: TextAlign.center,
),
const Gap(8),
Text(
'please_select_reason'.tr(),
style: AppTextStyles.bodyMediumReg
.copyWith(color: AppColors.blackGray),
textAlign: TextAlign.center,
),
const Gap(8),
CancelReasonDropdown(
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'.tr(),
hintText: 'enter_main_text'.tr(),
),
const Gap(24),
_buttonGroup(context),
],
),
),
),
),
);
}
Widget _buttonGroup(
BuildContext context,
) {
return Column(
children: [
KwButton.primary(
disabled: selectedReason.isEmpty,
label: 'submit_reason'.tr(),
onPressed: () {
context.maybePop({
'reason': selectedReason,
'additionalReason': _textEditingController.text,
});
},
),
const Gap(8),
KwButton.outlinedPrimary(
label: 'cancel'.tr(),
onPressed: () {
context.maybePop();
}),
],
);
}
}

View File

@@ -0,0 +1,114 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/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 = {
'sick_leave': 'sick_leave',
'vacation ': 'vacation',
'other': 'other_specify',
};
class CancelReasonDropdown extends StatelessWidget {
final String? selectedReason;
final Function(String reason) onReasonSelected;
const CancelReasonDropdown(
{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(
'reason'.tr(),
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.entries
.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(
reasons[selectedReason]?.tr() ?? 'select_reason_from_list'.tr(),
style: AppTextStyles.bodyMediumReg.copyWith(
color: selectedReason == null
? AppColors.blackGray
: AppColors.blackBlack),
)),
],
),
);
}
KwPopupMenuItem _buildMenuItem(BuildContext context,
MapEntry<String, String> entry, String selectedReason) {
return KwPopupMenuItem(
title: entry.value.tr(),
onTap: () {
onReasonSelected(entry.key);
},
icon: Container(
height: 16,
width: 16,
decoration: BoxDecoration(
color: selectedReason != entry.key ? null : AppColors.bgColorDark,
shape: BoxShape.circle,
border: selectedReason == entry.key
? null
: Border.all(color: AppColors.grayTintStroke, width: 1),
),
child: selectedReason == entry.key
? Center(
child: Assets.images.icons.check.svg(
height: 10,
width: 10,
colorFilter: const ColorFilter.mode(
AppColors.grayWhite, BlendMode.srcIn),
))
: null,
),
textStyle: AppTextStyles.bodySmallMed,
);
}
}

View File

@@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
import 'package:krow/features/shifts/presentation/dialogs/cancel_dialog/cancel_dialog_view.dart';
class ShiftCancelDialog extends StatelessWidget {
const ShiftCancelDialog({super.key});
static Future<Map<String, dynamic>?> showCustomDialog(
BuildContext context) async {
return await showDialog<Map<String, dynamic>>(
context: context,
builder: (context) => const CancelDialogView(),
);
}
@override
Widget build(BuildContext context) {
return const CancelDialogView();
}
}

View File

@@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow/features/shifts/domain/blocs/complete_dialog/shift_complete_dialog_bloc.dart';
import 'package:krow/features/shifts/domain/blocs/complete_dialog/shift_complete_dialog_event.dart';
import 'package:krow/features/shifts/presentation/dialogs/complete_dialog/widgets/complete_dialog_view.dart';
//TODO(Artem: create widgets instead helper methods. Add initial break time values.Move reasons to state. Create Time slots validator.)
class ShiftCompleteDialog {
static Future<Map<String, dynamic>?> showCustomDialog(BuildContext context,
bool canSkip, String eventName, DateTime minLimit, int breakDurationInMinutes) async {
return showDialog<Map<String, dynamic>>(
barrierDismissible: canSkip,
context: context,
builder: (context) => BlocProvider(
create: (_) => CompleteDialogBloc()
..add(
InitializeCompleteDialog(
minLimit: minLimit,
breakDurationInMinutes:breakDurationInMinutes , // Default break duration
),
),
child: CompleteDialogView(
canSkip: canSkip,
eventName: eventName,
),
),
);
}
}
class ClockOutDetails {
final String? breakStartTime;
final String? breakEndTime;
final String? reason;
final String? additionalReason;
ClockOutDetails._(
{this.breakStartTime,
this.breakEndTime,
this.reason,
this.additionalReason});
factory ClockOutDetails.positive(String breakStartTime, String breakEndTime) {
return ClockOutDetails._(
breakStartTime: breakStartTime,
breakEndTime: breakEndTime,
);
}
factory ClockOutDetails.negative(String reason, String additionalReason) {
return ClockOutDetails._(
reason: reason,
additionalReason: additionalReason,
);
}
factory ClockOutDetails.empty() {
return ClockOutDetails._();
}
}

View File

@@ -0,0 +1,57 @@
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/application/common/date_time_extension.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/kw_time_slot.dart';
import 'package:krow/features/shifts/domain/blocs/complete_dialog/shift_complete_dialog_bloc.dart';
import 'package:krow/features/shifts/domain/blocs/complete_dialog/shift_complete_dialog_event.dart';
import 'package:krow/features/shifts/domain/blocs/complete_dialog/shift_complete_dialog_state.dart';
class BreakTimePicker extends StatelessWidget {
final CompleteDialogState state;
const BreakTimePicker({super.key, required this.state});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: KwTimeSlotInput(
label: 'Break Start Time',
initialValue: DateFormat('H:mm').parse(state.startTime),
onChange: (v) => context.read<CompleteDialogBloc>().add(
ChangeStartTime(v),
),
),
),
const Gap(12),
Expanded(
child: KwTimeSlotInput(
editable: false,
label: 'Break End Time',
initialValue: DateFormat('H:mm').parse(state.endTime),
onChange: (v) {},
),
),
],
),
if (state.breakTimeInputError != null)
Padding(
padding: const EdgeInsets.only(top: 8, left: 16),
child: Text(
state.breakTimeInputError!,
style: AppTextStyles.bodyTinyReg
.copyWith(color: AppColors.statusError),
),
),
],
);
}
}

View File

@@ -0,0 +1,154 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/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/shifts/domain/blocs/complete_dialog/shift_complete_dialog_bloc.dart';
import 'package:krow/features/shifts/domain/blocs/complete_dialog/shift_complete_dialog_event.dart';
import 'package:krow/features/shifts/domain/blocs/complete_dialog/shift_complete_dialog_state.dart';
import 'package:krow/features/shifts/presentation/dialogs/complete_dialog/shift_complete_dialog.dart';
import 'package:krow/features/shifts/presentation/dialogs/complete_dialog/widgets/break_time_picker.dart';
import 'package:krow/features/shifts/presentation/dialogs/complete_dialog/widgets/complete_reason_input.dart';
class CompleteDialogView extends StatelessWidget {
CompleteDialogView({super.key, this.canSkip = true, required this.eventName});
final _textEditingController = TextEditingController();
final bool canSkip;
final String eventName;
String _title(BreakStatus state) => state == BreakStatus.negative
? 'help_us_understand'.tr()
: 'did_you_take_a_break'.tr();
String _message(BreakStatus state) => state == BreakStatus.negative
? 'taking_breaks_essential'.tr()
: 'taking_regular_breaks'.tr(namedArgs: {'eventName': eventName});
@override
Widget build(BuildContext context) {
return BlocBuilder<CompleteDialogBloc, CompleteDialogState>(
builder: (context, state) {
return Center(
child: AnimatedSize(
duration: const Duration(milliseconds: 300),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
decoration: BoxDecoration(
color: AppColors.grayWhite,
borderRadius: BorderRadius.circular(24),
),
child: SingleChildScrollView(
padding:
const EdgeInsets.symmetric(horizontal: 24, vertical: 56),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text(
_title(state.status),
style: AppTextStyles.headingH1.copyWith(height: 1),
textAlign: TextAlign.center,
),
const Gap(8),
Text(
_message(state.status),
style: AppTextStyles.bodyMediumReg
.copyWith(color: AppColors.blackGray),
textAlign: TextAlign.center,
),
const Gap(8),
if (state.status == BreakStatus.positive)
BreakTimePicker(state: state),
if (state.status == BreakStatus.negative)
CompleteReasonInput(selectedReason: state.selectedReason),
if (state.status == BreakStatus.negative) ...[
const Gap(8),
KwTextInput(
controller: _textEditingController,
minHeight: 144,
maxLength: 300,
showCounter: true,
radius: 12,
title: 'additional_reasons'.tr(),
hintText: 'enter_main_text'.tr(),
),
],
const Gap(24),
_buttonGroup(context, state),
],
),
),
),
),
);
},
);
}
Widget _buttonGroup(BuildContext context, CompleteDialogState state) {
var cancelButton = canSkip
? Padding(
padding: const EdgeInsets.only(top: 8.0),
child: KwButton.outlinedPrimary(
label: 'cancel'.tr(),
onPressed: () {
Navigator.pop(context);
},
),
)
: const SizedBox.shrink();
return Column(
children: [
if (state.status == BreakStatus.neutral) ...[
KwButton.primary(
label: 'yes_i_took_a_break'.tr(),
onPressed: () => context
.read<CompleteDialogBloc>()
.add(SelectBreakStatus(BreakStatus.positive)),
),
const Gap(8),
KwButton.outlinedPrimary(
label: 'no_i_didnt_take_a_break'.tr(),
onPressed: () => context
.read<CompleteDialogBloc>()
.add(SelectBreakStatus(BreakStatus.negative)),
),
],
if (state.status == BreakStatus.positive) ...[
KwButton.primary(
disabled: state.breakTimeInputError != null,
label: 'submit_break_time'.tr(),
onPressed: () {
Navigator.pop(context, <String, dynamic>{
'result': true,
'details': ClockOutDetails.positive(
state.startTime,
state.endTime,
)
});
},
),
cancelButton,
],
if (state.status == BreakStatus.negative) ...[
KwButton.primary(
disabled: state.selectedReason == null,
label: 'submit_reason'.tr(),
onPressed: () {
Navigator.pop(context, {
'result': false,
'details': ClockOutDetails.negative(
state.selectedReason!, _textEditingController.text)
});
},
),
cancelButton,
]
],
);
}
}

View File

@@ -0,0 +1,115 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_popup_menu.dart';
import 'package:krow/features/shifts/domain/blocs/complete_dialog/shift_complete_dialog_bloc.dart';
import 'package:krow/features/shifts/domain/blocs/complete_dialog/shift_complete_dialog_event.dart';
const reasons = {
'unpredictable_workflows': 'unpredictable_workflows',
'poor_time_management': 'poor_time_management',
'lack_of_coverage_or_short_staff': 'lack_of_coverage_or_short_staff',
'no_break_area': 'no_break_area',
'other': 'other',
};
class CompleteReasonInput extends StatelessWidget {
final String? selectedReason;
const CompleteReasonInput({super.key, required this.selectedReason});
@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(
'reason'.tr(),
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.entries
.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(
reasons[selectedReason]?.tr() ?? 'select_reason_from_list'.tr(),
style: AppTextStyles.bodyMediumReg.copyWith(
color: selectedReason == null
? AppColors.blackGray
: AppColors.blackBlack),
)),
],
),
);
}
KwPopupMenuItem _buildMenuItem(BuildContext context,
MapEntry<String, String> entry, String selectedReason) {
return KwPopupMenuItem(
title: entry.value.tr(),
onTap: () {
context.read<CompleteDialogBloc>().add(SelectReason(entry.key));
},
icon: Container(
height: 16,
width: 16,
decoration: BoxDecoration(
color: selectedReason != entry.key ? null : AppColors.bgColorDark,
shape: BoxShape.circle,
border: selectedReason == entry.key
? null
: Border.all(color: AppColors.grayTintStroke, width: 1),
),
child: selectedReason == entry.key
? Center(
child: Assets.images.icons.check.svg(
height: 10,
width: 10,
colorFilter: const ColorFilter.mode(
AppColors.grayWhite, BlendMode.srcIn),
))
: null,
),
textStyle: AppTextStyles.bodySmallMed,
);
}
}

View File

@@ -0,0 +1,105 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_input.dart';
import 'package:krow/features/shifts/presentation/dialogs/decline_dialog/decline_reason_dropdown.dart';
class DeclineDialogView extends StatefulWidget {
const DeclineDialogView({super.key});
@override
State<DeclineDialogView> createState() => _DeclineDialogViewState();
}
class _DeclineDialogViewState extends State<DeclineDialogView> {
String selectedReason = '';
final _textEditingController = TextEditingController();
@override
Widget build(BuildContext context) {
return Center(
child: AnimatedSize(
duration: const Duration(milliseconds: 300),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
decoration: BoxDecoration(
color: AppColors.grayWhite,
borderRadius: BorderRadius.circular(24),
),
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 56),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'decline_alert'.tr(),
style: AppTextStyles.headingH1.copyWith(height: 1),
textAlign: TextAlign.center,
),
const Gap(8),
Text(
'mention_reason_declining'.tr(),
style: AppTextStyles.bodyMediumReg
.copyWith(color: AppColors.blackGray),
textAlign: TextAlign.center,
),
const Gap(8),
DeclineReasonDropdown(
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'.tr(),
hintText: 'enter_main_text'.tr(),
),
const Gap(24),
_buttonGroup(context),
],
),
),
),
),
);
}
Widget _buttonGroup(
BuildContext context,
) {
return Column(
children: [
KwButton.primary(
disabled: selectedReason.isEmpty,
label: 'agree_and_close',
onPressed: () {
context.maybePop({
'reason': selectedReason,
'additionalReason': _textEditingController.text,
});
},
),
const Gap(8),
KwButton.outlinedPrimary(
label: 'contact_admin',
onPressed: () {
//todo contact admin
},
),
],
);
}
}

View File

@@ -0,0 +1,116 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/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 = {
'health': 'health',
'transportation': 'transportation',
'personal': 'personal',
'schedule_conflict': 'schedule_conflict',
'other': 'other_specify',
};
class DeclineReasonDropdown extends StatelessWidget {
final String? selectedReason;
final Function(String reason) onReasonSelected;
const DeclineReasonDropdown(
{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(
'valid_reasons'.tr(),
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.entries
.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(
reasons[selectedReason]?.tr() ?? 'select_reason_from_list'.tr(),
style: AppTextStyles.bodyMediumReg.copyWith(
color: selectedReason == null
? AppColors.blackGray
: AppColors.blackBlack),
)),
],
),
);
}
KwPopupMenuItem _buildMenuItem(BuildContext context,
MapEntry<String, String> entry, String selectedReason) {
return KwPopupMenuItem(
title: entry.value.tr(),
onTap: () {
onReasonSelected(entry.key);
},
icon: Container(
height: 16,
width: 16,
decoration: BoxDecoration(
color: selectedReason != entry.key ? null : AppColors.bgColorDark,
shape: BoxShape.circle,
border: selectedReason == entry.key
? null
: Border.all(color: AppColors.grayTintStroke, width: 1),
),
child: selectedReason == entry.key
? Center(
child: Assets.images.icons.check.svg(
height: 10,
width: 10,
colorFilter: const ColorFilter.mode(
AppColors.grayWhite, BlendMode.srcIn),
))
: null,
),
textStyle: AppTextStyles.bodySmallMed,
);
}
}

View File

@@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
import 'package:krow/features/shifts/presentation/dialogs/decline_dialog/decline_dialog_view.dart';
class ShiftDeclineDialog extends StatelessWidget {
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
Widget build(BuildContext context) {
return const DeclineDialogView();
}
}

View File

@@ -0,0 +1,107 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/widgets/ui_kit/dialogs/kw_dialog.dart';
import 'package:krow/features/shifts/domain/blocs/shift_deteils_bloc/shift_details_bloc.dart';
class GeocodingDialogs {
static bool dialogAlreadyOpen = false;
static void showGeocodingErrorDialog(
ShiftDetailsState state, BuildContext context) async{
if (dialogAlreadyOpen) {
return;
}
dialogAlreadyOpen = true;
var future;
switch (state.proximityState) {
case GeofencingProximityState.tooFar:
future = KwDialog.show(
context: context,
icon: Assets.images.icons.alertTriangle,
state: KwDialogState.warning,
title: "You're too far",
message: 'Please move closer to the designated location.',
primaryButtonLabel: 'OK',
onPrimaryButtonPressed: (dialogContext) {
dialogContext.router.maybePop();
dialogAlreadyOpen = false;
},
);
break;
case GeofencingProximityState.locationDisabled:
future = KwDialog.show(
context: context,
icon: Assets.images.icons.alertTriangle,
state: KwDialogState.negative,
title: 'Location Disabled',
message: 'Please enable location services to continue.',
primaryButtonLabel: 'Go to Settings',
onPrimaryButtonPressed: (dialogContext) async {
dialogContext.router.maybePop();
dialogAlreadyOpen = false;
await Geolocator.openLocationSettings();
},
);
break;
case GeofencingProximityState.goToSettings:
future = KwDialog.show(
context: context,
icon: Assets.images.icons.alertTriangle,
state: KwDialogState.info,
title: 'Permission Required',
message: 'You need to allow location access in settings.',
primaryButtonLabel: 'Open Settings',
onPrimaryButtonPressed: (dialogContext) async {
dialogContext.maybePop();
dialogAlreadyOpen = false;
await Geolocator.openLocationSettings();
},
);
break;
case GeofencingProximityState.onlyInUse:
future = KwDialog.show(
context: context,
icon: Assets.images.icons.alertTriangle,
state: KwDialogState.info,
title: 'Track "All the time" required',
message:
'To ensure accurate time tracking, we need access to your location. Time tracking will automatically stop if you move more than 500 meters away from your assigned work location. '
'Please grant “Allow all the time” access to your location. '
'Go to Settings → Permissions → Location and select “Allow all the time”.',
primaryButtonLabel: 'Open Settings',
onPrimaryButtonPressed: (dialogContext) async {
dialogContext.maybePop();
dialogAlreadyOpen = false;
await Geolocator.openLocationSettings();
},
);
break;
case GeofencingProximityState.permissionDenied:
future = KwDialog.show(
context: context,
icon: Assets.images.icons.alertTriangle,
state: KwDialogState.negative,
title: 'Permission Denied',
message:
'You have denied location access. Please allow it manually.',
primaryButtonLabel: 'OK',
onPrimaryButtonPressed: (dialogContext) async {
dialogContext.maybePop();
dialogAlreadyOpen = false;
await Geolocator.openAppSettings();
});
break;
default:
dialogAlreadyOpen = false;
break;
}
await future;
dialogAlreadyOpen = false;
}
}

View File

@@ -0,0 +1,212 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_app_bar.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_loading_overlay.dart';
import 'package:krow/features/shifts/data/models/staff_shift.dart';
import 'package:krow/features/shifts/domain/blocs/shift_deteils_bloc/shift_details_bloc.dart';
import 'package:krow/features/shifts/domain/shift_entity.dart';
import 'package:krow/features/shifts/presentation/dialogs/geocoding_dialogs.dart';
import 'package:krow/features/shifts/presentation/widgets/shift_buttons_widget.dart';
import 'package:krow/features/shifts/presentation/widgets/shift_info_time_row/shift_info_clock_time_widget.dart';
import 'package:krow/features/shifts/presentation/widgets/shift_info_time_row/shift_info_planing_duration_widget.dart';
import 'package:krow/features/shifts/presentation/widgets/shift_info_time_row/shift_info_start_widget.dart';
import 'package:krow/features/shifts/presentation/widgets/shift_info_time_row/shift_info_total_duration_widget.dart';
import 'package:krow/features/shifts/presentation/widgets/shift_item_widget.dart';
import 'package:krow/features/shifts/presentation/widgets/shift_key_responsibilities_widget.dart';
import 'package:krow/features/shifts/presentation/widgets/shift_location_widget.dart';
import 'package:krow/features/shifts/presentation/widgets/shift_manage_widget.dart';
import 'package:krow/features/shifts/presentation/widgets/shift_payment_step_card_widget.dart';
import 'package:krow/features/shifts/presentation/widgets/shift_rating_widget.dart';
import 'package:krow/features/shifts/presentation/widgets/shift_timer_widgets/shift_timer_card_widget.dart';
@RoutePage()
class ShiftDetailsScreen extends StatefulWidget implements AutoRouteWrapper {
final ShiftEntity shift;
const ShiftDetailsScreen({super.key, required this.shift});
@override
State<ShiftDetailsScreen> createState() => _ShiftDetailsScreenState();
@override
Widget wrappedRoute(BuildContext context) {
return BlocProvider<ShiftDetailsBloc>(
create: (context) =>
ShiftDetailsBloc()..add(ShiftDetailsInitialEvent(shift: shift)),
child: this,
);
}
}
class _ShiftDetailsScreenState extends State<ShiftDetailsScreen> with WidgetsBindingObserver{
final OverlayPortalController _controller = OverlayPortalController();
var expanded = false;
String _getTitle(EventShiftRoleStaffStatus status) {
switch (status) {
//do not use enum name. Its need for future localization;
case EventShiftRoleStaffStatus.assigned:
return 'assigned';
case EventShiftRoleStaffStatus.confirmed:
return 'confirmed';
case EventShiftRoleStaffStatus.ongoing:
return 'active';
case EventShiftRoleStaffStatus.completed:
return 'completed';
case EventShiftRoleStaffStatus.declineByStaff:
return 'declined';
case EventShiftRoleStaffStatus.canceledByStaff:
case EventShiftRoleStaffStatus.canceledByBusiness:
case EventShiftRoleStaffStatus.canceledByAdmin:
case EventShiftRoleStaffStatus.requestedReplace:
return 'canceled';
}
}
bool _showButtons(EventShiftRoleStaffStatus status) =>
status == EventShiftRoleStaffStatus.assigned || status == EventShiftRoleStaffStatus.confirmed;
bool showTimer(EventShiftRoleStaffStatus status) =>
status == EventShiftRoleStaffStatus.confirmed ||
status == EventShiftRoleStaffStatus.ongoing;
void _listenHandler(BuildContext context, ShiftDetailsState state) {
if (state.needPop) {
context.router.maybePop();
return;
}
if (state.isLoading) {
_controller.show();
} else {
_controller.hide();
}
if(state.errorMessage!=null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.errorMessage!),
),
);
}
if (state.proximityState == GeofencingProximityState.none) return;
GeocodingDialogs.showGeocodingErrorDialog(state, context);
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
expanded = widget.shift.status != EventShiftRoleStaffStatus.completed;
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
BlocProvider.of<ShiftDetailsBloc>(context).add(const ShiftCheckGeocodingEvent());
}
}
@override
Widget build(BuildContext context) {
return BlocConsumer<ShiftDetailsBloc, ShiftDetailsState>(
buildWhen: (previous, current) =>
previous.shiftViewModel != current.shiftViewModel,
listenWhen: (previous, current) =>
previous.isLoading != current.isLoading ||
previous.proximityState != current.proximityState || current.errorMessage!=null,
listener: _listenHandler,
builder: (context, state) {
var viewModel = state.shiftViewModel;
var status = viewModel.status;
return KwLoadingOverlay(
controller: _controller,
child: Scaffold(
appBar: KwAppBar(
titleText: '${_getTitle(status).tr()} ${'Shift'.tr()}',
showNotification: true,
),
body: Stack(
children: [
ListView(
padding: const EdgeInsets.only(top: 16),
children: [
ShiftItemWidget(
bottomPadding: 0,
viewModel,
isDetailsMode: true,
),
if (showTimer(viewModel.status))
const ShiftTimerCardWidget(),
_buildShiftTimeInfo(viewModel),
if (status == EventShiftRoleStaffStatus.completed)
ShiftPaymentStepCardWidget(viewModel: viewModel),
if (status == EventShiftRoleStaffStatus.completed &&
viewModel.rating != null)
ShiftRatingWidget(viewModel: viewModel),
if (status != EventShiftRoleStaffStatus.ongoing)
ShiftLocationWidget(viewModel: viewModel),
ShiftManageWidget(managers: viewModel.managers),
if (viewModel.additionalInfo != null)
ShiftKeyResponsibilitiesWidget(
text: viewModel.additionalInfo!,
expandable:
status == EventShiftRoleStaffStatus.completed,
isExpanded: expanded,
onTap: () {
setState(() {
expanded = !expanded;
});
},
),
SizedBox(height: MediaQuery.sizeOf(context).height / 3),
],
),
if (_showButtons(viewModel.status))
Positioned(
bottom: 0,
left: 0,
right: 0,
child: ShiftButtonsWidget(
viewModel.status,
),
),
],
),
),
);
},
);
}
Widget _buildShiftTimeInfo(ShiftEntity shiftViewModel) {
return Container(
margin: const EdgeInsets.only(left: 16, right: 16, top: 8),
child: Row(
children: [
shiftViewModel.status == EventShiftRoleStaffStatus.completed
? ShiftInfoClockTimeWidget(viewModel: shiftViewModel)
: ShiftInfoStartWidget(viewModel: shiftViewModel),
const Gap(8),
shiftViewModel.status == EventShiftRoleStaffStatus.completed
? ShiftInfoTotalDurationWidget(viewModel: shiftViewModel)
: ShiftInfoPlaningDurationWidget(viewModel: shiftViewModel)
],
),
);
}
}

View File

@@ -0,0 +1,204 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/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/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/shifts/domain/blocs/shifts_list_bloc/shifts_bloc.dart';
import 'package:krow/features/shifts/domain/blocs/shifts_list_bloc/shifts_event.dart';
import 'package:krow/features/shifts/domain/blocs/shifts_list_bloc/shifts_state.dart';
import 'package:krow/features/shifts/domain/services/shift_completer_service.dart';
import 'package:krow/features/shifts/domain/shift_entity.dart';
import 'package:krow/features/shifts/presentation/widgets/shift_item_widget.dart';
@RoutePage()
class ShiftsListMainScreen extends StatefulWidget {
const ShiftsListMainScreen({super.key});
@override
State<ShiftsListMainScreen> createState() => _ShiftsListMainScreenState();
}
class _ShiftsListMainScreenState extends State<ShiftsListMainScreen> {
late AppLifecycleListener _appLifecycleListener;
var dialogOpened = false;
final List<String> tabs = [
'assigned',
'confirmed',
'active',
'completed',
'canceled'
];
late ScrollController _scrollController;
@override
void didChangeDependencies() {
super.didChangeDependencies();
WidgetsBinding.instance.addPostFrameCallback((_) {
BlocProvider.of<ShiftsBloc>(context).add(
LoadTabShiftEvent(
status: BlocProvider.of<ShiftsBloc>(context).state.tabIndex),
);
BlocProvider.of<ShiftsBloc>(context).add(
const ReloadMissingBreakShift(),
);
});
}
@override
void initState() {
super.initState();
_appLifecycleListener = AppLifecycleListener(onStateChange: (state) {
if (state == AppLifecycleState.resumed) {
BlocProvider.of<ShiftsBloc>(context).add(
const ReloadMissingBreakShift(),
);
}
});
_scrollController = ScrollController();
_scrollController.addListener(_onScroll);
}
@override
void dispose() {
_appLifecycleListener.dispose();
_scrollController.removeListener(_onScroll);
_scrollController.dispose();
super.dispose();
}
void _onScroll() {
if (_scrollController.position.atEdge) {
if (_scrollController.position.pixels != 0) {
BlocProvider.of<ShiftsBloc>(context).add(
LoadMoreShiftEvent(
status: BlocProvider.of<ShiftsBloc>(context).state.tabIndex),
);
}
}
}
@override
Widget build(BuildContext context) {
return BlocConsumer<ShiftsBloc, ShiftsState>(
listenWhen: (oldState, state) {
return state.missedShifts.isNotEmpty &&
(oldState.missedShifts.isEmpty ||
oldState.missedShifts.first.id != state.missedShifts.first.id);
},
listener: (context, state) async {
if (state.missedShifts.isNotEmpty) {
if(dialogOpened) return;
dialogOpened = true;
await ShiftCompleterService().startCompleteProcess(
context,
canSkip: false,
state.missedShifts.first,
onComplete: () {},
);
dialogOpened = false;
BlocProvider.of<ShiftsBloc>(context).add(
const ReloadMissingBreakShift(),
);
}
},
builder: (context, state) {
List<ShiftEntity> items = state.tabs[state.tabIndex]!.items;
return Scaffold(
appBar: KwAppBar(
titleText: 'your_shifts'.tr(),
showNotification: true,
centerTitle: false,
),
body: ScrollLayoutHelper(
padding: const EdgeInsets.symmetric(vertical: 16),
onRefresh: () async {
BlocProvider.of<ShiftsBloc>(context)
.add(const ReloadMissingBreakShift());
BlocProvider.of<ShiftsBloc>(context)
.add(LoadTabShiftEvent(status: state.tabIndex));
},
controller: _scrollController,
upperWidget: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
KwTabBar(
key: const Key('shifts_tab_bar'),
tabs: tabs.map((e) => e.tr()).toList(),
onTap: (index) {
BlocProvider.of<ShiftsBloc>(context)
.add(ShiftsTabChangedEvent(tabIndex: index));
}),
const Gap(16),
if (state.tabs[state.tabIndex]!.isLoading &&
state.tabs[state.tabIndex]!.items.isEmpty)
..._buildListLoading(),
if (!state.tabs[state.tabIndex]!.isLoading && items.isEmpty)
..._emptyListWidget(),
RefreshIndicator(
onRefresh: () async {
BlocProvider.of<ShiftsBloc>(context)
.add(LoadTabShiftEvent(status: state.tabIndex));
},
child: ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: items.length,
itemBuilder: (context, index) {
return ShiftItemWidget(
items[index],
bottomPadding: 12,
onPressed: () {
context.pushRoute(
ShiftDetailsRoute(shift: 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),
Text(
'you_currently_have_no_shifts'.tr(),
textAlign: TextAlign.center,
style: AppTextStyles.headingH2,
),
];
}
}

View File

@@ -0,0 +1,19 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow/features/shifts/domain/blocs/shifts_list_bloc/shifts_bloc.dart';
import 'package:krow/features/shifts/domain/blocs/shifts_list_bloc/shifts_event.dart';
@RoutePage()
class ShiftsFlowScreen extends StatelessWidget {
const ShiftsFlowScreen({super.key});
@override
Widget build(BuildContext context) {
return MultiBlocProvider(providers: [
BlocProvider<ShiftsBloc>(
create: (context) => ShiftsBloc()..add(const ShiftsInitialEvent()),
),
], child: const AutoRouter());
}
}

View File

@@ -0,0 +1,76 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
import 'package:krow/features/shifts/data/models/staff_shift.dart';
import 'package:krow/features/shifts/domain/blocs/shift_deteils_bloc/shift_details_bloc.dart';
import 'package:krow/features/shifts/presentation/dialogs/cancel_dialog/shift_cancel_dialog.dart';
import 'package:krow/features/shifts/presentation/dialogs/decline_dialog/shift_decline_dialog.dart';
class ShiftButtonsWidget extends StatelessWidget {
final EventShiftRoleStaffStatus status;
const ShiftButtonsWidget(this.status, {super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: const Alignment(0, -0.5),
colors: [
AppColors.grayWhite,
AppColors.grayWhite.withAlpha(0),
],
),
),
child: SafeArea(
child: Column(
children: status == EventShiftRoleStaffStatus.assigned
? [
KwButton.primary(
label: 'accept_shift'.tr(),
onPressed: () {
BlocProvider.of<ShiftDetailsBloc>(context).add(
const ShiftConfirmEvent(),
);
},
fit: KwButtonFit.expanded,
),
const Gap(12),
KwButton.outlinedPrimary(
label: 'decline_shift'.tr(),
onPressed: () async {
var result =
await ShiftDeclineDialog.showCustomDialog(context);
if (result != null && context.mounted) {
BlocProvider.of<ShiftDetailsBloc>(context).add(
ShiftDeclineEvent(
result['reason'], result['additionalReason']),
);
}
}).copyWith(color: AppColors.statusError)
]
: [
KwButton.outlinedPrimary(
label: 'cancel_shift'.tr(),
onPressed: () async {
var result =
await ShiftCancelDialog.showCustomDialog(context);
if (result != null && context.mounted) {
BlocProvider.of<ShiftDetailsBloc>(context).add(
ShiftCancelEvent(
result['reason'], result['additionalReason']),
);
}
}).copyWith(color: AppColors.statusError)
],
),
),
);
}
}

View File

@@ -0,0 +1,51 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:intl/intl.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/shifts/domain/shift_entity.dart';
class ShiftInfoClockTimeWidget extends StatelessWidget {
final ShiftEntity viewModel;
const ShiftInfoClockTimeWidget({super.key, required this.viewModel});
@override
Widget build(BuildContext context) {
var clockIn = DateFormat('h:mma','en')
.format(viewModel.clockIn ?? DateTime.now())
.toLowerCase();
var clockOut = DateFormat('h:mma','en')
.format(viewModel.clockOut ?? DateTime.now())
.toLowerCase();
return Expanded(
child: Container(
padding: const EdgeInsets.all(12),
decoration: KwBoxDecorations.primaryLight12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${'clock_in_1'.tr()} - ${'clock_out_1'.tr()}'.toUpperCase(),
style: AppTextStyles.captionReg
.copyWith(color: AppColors.blackCaptionText)),
const Gap(16),
Text(clockIn, style: AppTextStyles.headingH3),
const Gap(6),
Text('clock_in_1'.tr(),
style: AppTextStyles.bodySmallReg
.copyWith(color: AppColors.blackGray)),
const Gap(12),
Text(clockOut, style: AppTextStyles.headingH3),
const Gap(6),
Text('clock_out_1'.tr(),
style: AppTextStyles.bodySmallReg
.copyWith(color: AppColors.blackGray)),
],
),
),
);
}
}

View File

@@ -0,0 +1,54 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/features/shifts/domain/shift_entity.dart';
class ShiftInfoPlaningDurationWidget extends StatelessWidget {
final ShiftEntity viewModel;
const ShiftInfoPlaningDurationWidget({super.key, required this.viewModel});
@override
Widget build(BuildContext context) {
var formattedDuration =
_format(viewModel.endDate.difference(viewModel.startDate).inMinutes);
var formattedBreak = _format(viewModel.planingBreakTime!);
return Expanded(
child: Container(
padding: const EdgeInsets.all(12),
decoration: KwBoxDecorations.primaryLight12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('duration'.tr().toUpperCase(),
style: AppTextStyles.captionReg
.copyWith(color: AppColors.blackCaptionText)),
const Gap(16),
Text(formattedDuration, style: AppTextStyles.headingH3),
const Gap(6),
Text('shift_duration'.tr(),
style: AppTextStyles.bodySmallReg
.copyWith(color: AppColors.blackGray)),
const Gap(12),
Text(formattedBreak, style: AppTextStyles.headingH3),
const Gap(6),
Text('break_duration'.tr(),
style: AppTextStyles.bodySmallReg
.copyWith(color: AppColors.blackGray)),
],
),
),
);
}
String _format(int? duration) {
if (duration == null) return '0 hours';
var hours = duration ~/ 60;
var minutes = duration % 60;
var hoursStr = 'hours_1'.tr(namedArgs: {'hours':hours.toString(),'plural':hours == 1 ? '' : 's'});
return minutes == 0 ? hoursStr : 'hours_minutes'.tr(namedArgs: {'hours':hours.toString(),'minutes':minutes.toString()});
}
}

View File

@@ -0,0 +1,48 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:intl/intl.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/shifts/domain/shift_entity.dart';
class ShiftInfoStartWidget extends StatelessWidget {
final ShiftEntity viewModel;
const ShiftInfoStartWidget({super.key, required this.viewModel});
@override
Widget build(BuildContext context) {
var dateTime = viewModel.startDate;
var date = DateFormat('MMMM d', context.locale.languageCode).format(dateTime);
var time = DateFormat('h:mma', 'en').format(dateTime).toLowerCase();
return Expanded(
child: Container(
padding: const EdgeInsets.all(12),
decoration: KwBoxDecorations.primaryLight12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('start'.tr().toUpperCase(),
style: AppTextStyles.captionReg
.copyWith(color: AppColors.blackCaptionText)),
const Gap(16),
Text(date, style: AppTextStyles.headingH3),
const Gap(6),
Text('date'.tr(),
style: AppTextStyles.bodySmallReg
.copyWith(color: AppColors.blackGray)),
const Gap(12),
Text(time, style: AppTextStyles.headingH3),
const Gap(6),
Text('time'.tr(),
style: AppTextStyles.bodySmallReg
.copyWith(color: AppColors.blackGray)),
],
),
),
);
}
}

View File

@@ -0,0 +1,60 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/features/shifts/domain/shift_entity.dart';
class ShiftInfoTotalDurationWidget extends StatelessWidget {
final ShiftEntity viewModel;
const ShiftInfoTotalDurationWidget({super.key, required this.viewModel});
@override
Widget build(BuildContext context) {
var formattedDuration;
if (viewModel.clockOut == null || viewModel.clockIn == null) {
formattedDuration = '00:00:00';
}
formattedDuration = _format(viewModel.clockOut
?.difference(viewModel.clockIn ?? DateTime.now())
.inSeconds ??
0);
var formattedBreak = _format(viewModel.totalBreakTime!);
return Expanded(
child: Container(
padding: const EdgeInsets.all(12),
decoration: KwBoxDecorations.primaryLight12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('total_time_breaks'.tr().toUpperCase(),
style: AppTextStyles.captionReg
.copyWith(color: AppColors.blackCaptionText)),
const Gap(16),
Text(formattedBreak, style: AppTextStyles.headingH3),
const Gap(6),
Text('break_hours'.tr(),
style: AppTextStyles.bodySmallReg
.copyWith(color: AppColors.blackGray)),
const Gap(12),
Text(formattedDuration, style: AppTextStyles.headingH3),
const Gap(6),
Text('total_hours'.tr(),
style: AppTextStyles.bodySmallReg
.copyWith(color: AppColors.blackGray)),
],
),
),
);
}
String _format(int duration) {
var hours = (duration ~/ 3600).toString().padLeft(2, '0');
var minutes = ((duration % 3600) ~/ 60).toString().padLeft(2, '0');
var seconds = (duration % 60).toString().padLeft(2, '0');
return '$hours:$minutes:$seconds';
}
}

View File

@@ -0,0 +1,99 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/features/shifts/data/models/cancellation_reason.dart';
class ShiftItemCanceledWidget extends StatelessWidget {
final CancellationReason? reason;
final bool canceledByUser;
const ShiftItemCanceledWidget(
{super.key, required this.reason, required this.canceledByUser});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(12.0),
margin: const EdgeInsets.only(left: 16, right: 16, top: 8),
decoration:
KwBoxDecorations.primaryLight8.copyWith(color: AppColors.tintBlue),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'your_shift_canceled'.tr(),
style: AppTextStyles.bodyMediumMed
.copyWith(color: AppColors.primaryBlue),
),
],
),
const Gap(8),
Text(
'please_review_reason'.tr(),
style: AppTextStyles.bodyTinyReg
.copyWith(color: AppColors.primaryBlue)),
const Gap(8),
Row(
children: [
Text(
'canceled_by'.tr(),
style: AppTextStyles.bodyTinyReg
.copyWith(color: AppColors.tintDarkBlue),
),
const Gap(4),
Text(
canceledByUser ? 'user'.tr() : 'admin'.tr(),
style: AppTextStyles.bodyTinyReg
.copyWith(color: AppColors.primaryBlue),
),
],
),
const Gap(8),
Container(
padding: const EdgeInsets.all(12.0),
decoration: KwBoxDecorations.primaryLight6,
child: Row(
children: [
Text(
'${'reason'.tr()}:',
style: AppTextStyles.bodyMediumReg
.copyWith(color: AppColors.blackGray),
),
const Gap(4),
Text(reasonToString(reason).tr(),
style: AppTextStyles.bodyMediumMed),
],
),
),
],
),
);
}
String reasonToString(CancellationReason? reason) {
switch (reason) {
case CancellationReason.sickLeave:
return 'sick_leave';
case CancellationReason.vacation:
return 'vacation';
case CancellationReason.other:
return 'Other';
case CancellationReason.health:
return 'health';
case CancellationReason.transportation:
return 'transportation';
case CancellationReason.personal:
return 'personal';
case CancellationReason.scheduleConflict:
return 'schedule_conflict';
case null:
return 'other';
}
}
}

View File

@@ -0,0 +1,106 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/features/shifts/domain/shift_entity.dart';
class ShiftAdditionalDetails extends StatefulWidget {
final ShiftEntity viewModel;
const ShiftAdditionalDetails({super.key, required this.viewModel});
@override
State<ShiftAdditionalDetails> createState() => _ShiftAdditionalDetailsState();
}
class _ShiftAdditionalDetailsState extends State<ShiftAdditionalDetails> {
bool expanded = false;
@override
Widget build(BuildContext context) {
if (widget.viewModel.additionalData?.isEmpty ?? true) {
return const Gap(12);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Gap(12),
Container(
margin: const EdgeInsets.symmetric(horizontal: 20),
height: 1,
color: AppColors.grayTintStroke,
),
const Gap(12),
GestureDetector(
onTap: () {
setState(() {
expanded = !expanded;
});
},
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 20),
height: 16,
color: Colors.transparent,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text('additional_details'.tr(),
style: AppTextStyles.captionReg),
const Spacer(),
AnimatedRotation(
duration: const Duration(milliseconds: 150),
turns: expanded ? -0.5 : 0,
child: Assets.images.icons.caretDown.svg(
width: 16,
height: 16,
colorFilter: const ColorFilter.mode(
AppColors.blackBlack, BlendMode.srcIn))),
],
),
),
),
if (widget.viewModel.additionalData?.isNotEmpty ?? false) ...[
const Gap(12),
_buildExpandedDetails(),
]
],
);
}
_buildExpandedDetails() {
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
clipBehavior: Clip.antiAlias,
height: expanded ? widget.viewModel.additionalData!.length * 28 + 12 : 0,
padding: const EdgeInsets.only(left: 20, right: 20, bottom: 12),
decoration: const BoxDecoration(
color: AppColors.graySecondaryFrame,
borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
...widget.viewModel.additionalData?.map(
(e) {
return Padding(
padding: const EdgeInsets.only(top: 12),
child: Row(
children: [
Text(e.name ?? '',
style: AppTextStyles.bodySmallReg
.copyWith(color: AppColors.blackGray)),
const Spacer(),
Text('yes'.tr(), style: AppTextStyles.bodySmallReg),
],
),
);
},
) ??
[],
],
),
);
}
}

View File

@@ -0,0 +1,62 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/features/shifts/domain/shift_entity.dart';
import 'package:krow/features/shifts/presentation/widgets/shift_item_components/shift_status_label_widget.dart';
class ShiftItemHeaderWidget extends StatelessWidget {
final ShiftEntity viewModel;
const ShiftItemHeaderWidget(this.viewModel, {super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
viewModel.imageUrl.isNotEmpty
? ClipOval(
child: CachedNetworkImage(
imageUrl: viewModel.imageUrl,
fit: BoxFit.cover,
height: 48,
width: 48,
))
: const SizedBox.shrink(),
ShiftStatusLabelWidget(viewModel),
],
),
const Gap(12),
Row(
children: [
Expanded(
child: Text(
viewModel.skillName,
style: AppTextStyles.bodyMediumMed,
)),
const Gap(24),
Text(
'\$${viewModel.rate}/h',
style: AppTextStyles.bodyMediumMed,
)
],
),
const Gap(4),
Text(
viewModel.eventName,
style:
AppTextStyles.bodySmallReg.copyWith(color: AppColors.blackGray),
),
],
),
);
}
}

View File

@@ -0,0 +1,98 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/features/shifts/domain/shift_entity.dart';
Timer? shiftListTimer;
class ShiftOngoingCounterWidget extends StatefulWidget {
final ShiftEntity viewModel;
const ShiftOngoingCounterWidget({super.key, required this.viewModel});
@override
State<ShiftOngoingCounterWidget> createState() =>
_ShiftOngoingCounterWidgetState();
}
class _ShiftOngoingCounterWidgetState extends State<ShiftOngoingCounterWidget> {
@override
void initState() {
super.initState();
shiftListTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (mounted) {
setState(() {});
}
});
}
@override
Widget build(BuildContext context) {
var duration =
DateTime.now().difference(widget.viewModel.clockIn ?? DateTime.now());
var hours = duration.inHours.remainder(24).abs().toString().padLeft(2, '0');
var minutes =
duration.inMinutes.remainder(60).abs().toString().padLeft(2, '0');
var seconds =
duration.inSeconds.remainder(60).abs().toString().padLeft(2, '0');
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
height: 80,
margin: const EdgeInsets.only(top: 24, left: 24, right: 24),
decoration: BoxDecoration(
color: AppColors.tintGreen,
border: Border.all(color: AppColors.tintDarkGreen),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildTimeText('hours'.tr(), hours),
_divider(),
_buildTimeText('minutes'.tr(), minutes),
_divider(),
_buildTimeText('seconds'.tr(), seconds),
],
),
),
],
);
}
Widget _divider() {
return SizedBox(
width: 24,
child: Center(
child: Container(
height: 24,
width: 1,
color: AppColors.tintDarkGreen,
),
),
);
}
Widget _buildTimeText(String label, String time) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(time,
style: AppTextStyles.headingH3
.copyWith(color: AppColors.statusSuccess)),
const Gap(6),
Text(
label,
style: AppTextStyles.bodySmallReg
.copyWith(color: AppColors.statusSuccess),
),
],
);
}
}

View File

@@ -0,0 +1,77 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/features/shifts/domain/shift_entity.dart';
class ShiftPlaceAndTimeWidget extends StatelessWidget {
final ShiftEntity viewModel;
const ShiftPlaceAndTimeWidget(this.viewModel, {super.key});
@override
Widget build(BuildContext context) {
final timeFormat = DateFormat('MMMM d, h:mma', context.locale.languageCode);
return Stack(
children: [
SizedBox(
height: 28,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(top: 12.0),
child: Container(
constraints: BoxConstraints(
minWidth: MediaQuery.of(context).size.width - 32),
child: IntrinsicWidth(
child: Row(
children: [
const Gap(16),
Assets.images.icons.location.svg(width: 16, height: 16),
const Gap(4),
Text(
viewModel.locationName,
style: AppTextStyles.bodySmallReg,
),
const Spacer(),
const Gap(12),
Assets.images.icons.calendar.svg(width: 16, height: 16),
const Gap(4),
Text(
timeFormat.format(viewModel.startDate),
style: AppTextStyles.bodySmallReg,
),
const Gap(16),
],
),
),
),
);
},
),
),
Positioned(
right: 0,
child: Container(
height: 28,
width: 20,
decoration: BoxDecoration(
color: Colors.red,
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
AppColors.grayPrimaryFrame.withAlpha(50),
AppColors.grayPrimaryFrame.withAlpha(255),
],
),
),
),
),
],
);
}
}

View File

@@ -0,0 +1,104 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/features/shifts/data/models/staff_shift.dart';
import 'package:krow/features/shifts/domain/shift_entity.dart';
class ShiftStatusLabelWidget extends StatefulWidget {
final ShiftEntity viewModel;
const ShiftStatusLabelWidget(this.viewModel, {super.key});
@override
State<ShiftStatusLabelWidget> createState() => _ShiftStatusLabelWidgetState();
}
class _ShiftStatusLabelWidgetState extends State<ShiftStatusLabelWidget> {
Color getColor() {
switch (widget.viewModel.status) {
case EventShiftRoleStaffStatus.assigned:
return AppColors.primaryBlue;
case EventShiftRoleStaffStatus.confirmed:
return AppColors.statusWarning;
case EventShiftRoleStaffStatus.ongoing:
return AppColors.statusSuccess;
case EventShiftRoleStaffStatus.completed:
return AppColors.bgColorDark;
case EventShiftRoleStaffStatus.canceledByAdmin:
case EventShiftRoleStaffStatus.canceledByBusiness:
case EventShiftRoleStaffStatus.canceledByStaff:
case EventShiftRoleStaffStatus.requestedReplace:
case EventShiftRoleStaffStatus.declineByStaff:
return AppColors.statusError;
}
}
String getText() {
switch (widget.viewModel.status) {
case EventShiftRoleStaffStatus.assigned:
return _getAssignedAgo();
case EventShiftRoleStaffStatus.confirmed:
return _getStartIn();
case EventShiftRoleStaffStatus.ongoing:
return 'ongoing'.tr();
case EventShiftRoleStaffStatus.completed:
return 'completed'.tr();
case EventShiftRoleStaffStatus.declineByStaff:
return 'declined'.tr();
case EventShiftRoleStaffStatus.canceledByAdmin:
case EventShiftRoleStaffStatus.canceledByBusiness:
case EventShiftRoleStaffStatus.canceledByStaff:
case EventShiftRoleStaffStatus.requestedReplace:
return 'canceled'.tr();
}
}
String _getStartIn() {
var duration = widget.viewModel.startDate.difference(DateTime.now());
var startIn = '';
if (duration.inMinutes < 0) {
return 'started'.tr();
}
if (duration.inDays > 0) {
startIn = '${duration.inDays}d ${duration.inHours.remainder(24)}h';
} else if (duration.inHours.abs() > 0) {
startIn = '${duration.inHours}h ${duration.inMinutes.remainder(60)}min';
} else {
startIn = '${duration.inMinutes}min';
}
return 'starts_in'.tr(namedArgs: {'time': startIn});
}
String _getAssignedAgo() {
var duration = DateTime.now().difference(widget.viewModel.assignedDate);
var timeAgo = '';
if (duration.inDays > 0) {
timeAgo = '${duration.inDays}d ago';
} else if (duration.inHours > 0) {
timeAgo = '${duration.inHours}h ago';
} else {
timeAgo = '${duration.inMinutes}m ago';
}
return 'assigned_ago'.tr(namedArgs: {'time': timeAgo});
}
@override
Widget build(BuildContext context) {
return Container(
height: 24,
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: getColor(),
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Text(
getText(),
style: AppTextStyles.bodySmallMed
.copyWith(color: AppColors.grayWhite, height: 0.7),
),
),
);
}
}

View File

@@ -0,0 +1,104 @@
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/features/shifts/data/models/event_tag.dart';
import 'package:krow/features/shifts/domain/shift_entity.dart';
class ShiftTagsWidget extends StatelessWidget {
final ShiftEntity viewModel;
final bool scrollable;
const ShiftTagsWidget(this.viewModel, {this.scrollable = false, super.key});
final textColors = const {
'1': AppColors.statusWarningBody,
'2': AppColors.statusError,
'3': AppColors.statusSuccess,
};
final backGroundsColors = const {
'1': AppColors.tintYellow,
'2': AppColors.tintRed,
'3': AppColors.tintGreen,
};
@override
Widget build(BuildContext context) {
if (viewModel.tags == null || viewModel.tags!.isEmpty) {
return const SizedBox.shrink();
}
return Column(
children: [
const Gap(12),
if (scrollable) _buildScrollableList(),
if (!scrollable) _buildChips(),
],
);
}
Widget _buildScrollableList() {
return SizedBox(
height: 44,
child: ListView.builder(
padding: const EdgeInsets.only(left: 16, right: 12),
itemCount: viewModel.tags?.length ?? 0,
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return _buildTag(viewModel.tags![index]);
},
),
);
}
Widget _buildChips() {
return Padding(
padding: const EdgeInsets.only(left: 16, right: 12),
child: Wrap(
runSpacing: 4,
children: viewModel.tags!.map((e) => _buildTag(e)).toList(),
),
);
}
Widget _buildTag(EventTag shiftTage) {
return Container(
margin: const EdgeInsets.only(right: 4),
padding: const EdgeInsets.all(8),
height: 44,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: backGroundsColors[shiftTage.id]??AppColors.tintGreen,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
height: 28,
width: 28,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: AppColors.grayWhite,
),
child: Center(
child: Assets.images.icons.eye.svg(
width: 12,
height: 12,
colorFilter: ColorFilter.mode(
textColors[shiftTage.id] ?? AppColors.statusWarningBody,
BlendMode.srcIn)),
),
),
const Gap(8),
Text(
shiftTage.name,
style: AppTextStyles.bodySmallReg
.copyWith(color: textColors[shiftTage.id]??AppColors.statusSuccess),
),
const Gap(8),
],
),
);
}
}

View File

@@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
import 'package:krow/core/presentation/widgets/shift_payment_step_widget.dart';
import 'package:krow/core/presentation/widgets/shift_total_time_spend_widget.dart';
import 'package:krow/features/shifts/data/models/staff_shift.dart';
import 'package:krow/features/shifts/domain/shift_entity.dart';
import 'package:krow/features/shifts/presentation/widgets/shift_item_canceled_widget.dart';
import 'package:krow/features/shifts/presentation/widgets/shift_item_components/shift_additional_details_widget.dart';
import 'package:krow/features/shifts/presentation/widgets/shift_item_components/shift_item_header_widget.dart';
import 'package:krow/features/shifts/presentation/widgets/shift_item_components/shift_ongoing_counter_widget.dart';
import 'package:krow/features/shifts/presentation/widgets/shift_item_components/shift_place_and_time_widget.dart';
import 'package:krow/features/shifts/presentation/widgets/shift_item_components/shift_tags_widget.dart';
class ShiftItemWidget extends StatelessWidget {
final ShiftEntity viewModel;
final bool isDetailsMode;
final VoidCallback? onPressed;
final double bottomPadding;
const ShiftItemWidget(this.viewModel,
{this.bottomPadding = 0,
this.onPressed,
this.isDetailsMode = false,
super.key});
@override
Widget build(BuildContext context) {
var showPlaceAndTime = !isDetailsMode &&
(viewModel.status == EventShiftRoleStaffStatus.assigned ||
viewModel.status == EventShiftRoleStaffStatus.confirmed);
var showTags =
isDetailsMode || viewModel.status == EventShiftRoleStaffStatus.assigned;
var showOngoingCounter =
!isDetailsMode && viewModel.status == EventShiftRoleStaffStatus.ongoing;
var showTotalTimeSpend = !isDetailsMode &&
viewModel.status == EventShiftRoleStaffStatus.completed;
var showAdditionalDetails =
isDetailsMode || viewModel.status == EventShiftRoleStaffStatus.ongoing;
var canceled =
viewModel.status == EventShiftRoleStaffStatus.canceledByStaff ||
viewModel.status == EventShiftRoleStaffStatus.canceledByBusiness ||
viewModel.status == EventShiftRoleStaffStatus.canceledByAdmin;
return GestureDetector(
onTap: canceled?null:onPressed,
child: Container(
decoration: KwBoxDecorations.primaryLight12,
margin: EdgeInsets.only(bottom: bottomPadding, left: 16, right: 16),
padding:
EdgeInsets.only(top: 24, bottom: showAdditionalDetails ? 0 : 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ShiftItemHeaderWidget(viewModel),
if (showPlaceAndTime) ShiftPlaceAndTimeWidget(viewModel),
if (showTags)
ShiftTagsWidget(viewModel, scrollable: !isDetailsMode),
if (showOngoingCounter)
ShiftOngoingCounterWidget(viewModel: viewModel),
if (showTotalTimeSpend)
ShiftTotalTimeSpendWidget(
startTime: viewModel.clockIn?? DateTime.now(),
endTime: viewModel.clockOut?? viewModel.clockIn??DateTime.now(),
totalBreakTime: viewModel.totalBreakTime ?? 0,
),
if (showTotalTimeSpend)
const ShiftPaymentStepWidget(
currentIndex: 1,
),
if (showAdditionalDetails)
ShiftAdditionalDetails(viewModel: viewModel),
if (canceled)
ShiftItemCanceledWidget(
reason: viewModel.cancellationReason,
canceledByUser: viewModel.status ==
EventShiftRoleStaffStatus.canceledByStaff),
],
),
),
);
}
}

View File

@@ -0,0 +1,90 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
class ShiftKeyResponsibilitiesWidget extends StatelessWidget {
final String text;
final bool isExpanded;
final bool expandable;
final VoidCallback onTap;
const ShiftKeyResponsibilitiesWidget(
{super.key,
required this.text,
required this.expandable,
required this.isExpanded,
required this.onTap});
@override
Widget build(BuildContext context) {
return AnimatedSize(
duration: const Duration(milliseconds: 300),
clipBehavior: Clip.antiAlias,
alignment: Alignment.topCenter,
child: Container(
clipBehavior: Clip.antiAlias,
height: isExpanded ? null : 40,
margin: const EdgeInsets.only(top: 8, left: 16, right: 16),
padding: const EdgeInsets.all(12),
decoration: KwBoxDecorations.primaryLight12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
expandable ? expandableTitle() : fixedTitle(),
const Gap(16),
Text(
text,
style: AppTextStyles.bodySmallReg
.copyWith(color: AppColors.blackGray),
),
],
),
),
);
}
Widget fixedTitle() {
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text('${'key_responsibilities'.tr()}:'.toUpperCase(),
style: AppTextStyles.captionReg
.copyWith(color: AppColors.blackCaptionText)),
]);
}
Widget expandableTitle() {
return GestureDetector(
onTap: onTap,
child: Container(
color: Colors.transparent,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('additional_information'.tr().toUpperCase(),
style: AppTextStyles.bodySmallReg
.copyWith(color: AppColors.blackGray)),
],
),
AnimatedRotation(
duration: const Duration(milliseconds: 150),
turns: isExpanded ? -0.5 : 0,
child: Assets.images.icons.caretDown.svg(
width: 16,
height: 16,
colorFilter: const ColorFilter.mode(
AppColors.blackBlack, BlendMode.srcIn))),
],
),
),
);
}
}

View File

@@ -0,0 +1,106 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:krow/core/application/common/map_utils.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/features/shifts/data/models/staff_shift.dart';
import 'package:krow/features/shifts/domain/shift_entity.dart';
class ShiftLocationWidget extends StatelessWidget {
final ShiftEntity viewModel;
const ShiftLocationWidget({super.key, required this.viewModel});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(top: 8, left: 16, right: 16),
padding: const EdgeInsets.all(12),
decoration: KwBoxDecorations.primaryLight12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'location'.tr().toUpperCase(),
style: AppTextStyles.captionReg
.copyWith(color: AppColors.blackCaptionText),
),
const Gap(16),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Text(
viewModel.locationName,
style: AppTextStyles.headingH3.copyWith(height: 1),
),
),
),
const Gap(16),
KwButton.outlinedPrimary(
label: 'get_direction'.tr(),
leftIcon: Assets.images.icons.routing,
onPressed: () {
MapUtils.openMapByLatLon(
viewModel.locationLat,
viewModel.locationLon,
);
},
),
],
),
if (viewModel.status != EventShiftRoleStaffStatus.completed)
_buildMap(),
],
),
);
}
Widget _buildMap() {
if (viewModel.locationLat == 0 && viewModel.locationLon == 0) {
return Container(
height: 166,
margin: const EdgeInsets.only(top: 16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
),
);
}
return Container(
margin: const EdgeInsets.only(top: 16),
height: 166,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: GoogleMap(
rotateGesturesEnabled: false,
compassEnabled: false,
zoomControlsEnabled: false,
scrollGesturesEnabled: false,
zoomGesturesEnabled: false,
tiltGesturesEnabled: false,
mapToolbarEnabled: false,
myLocationButtonEnabled: false,
markers: {
Marker(
markerId: const MarkerId('1'),
position: LatLng(viewModel.locationLat, viewModel.locationLon),
),
},
initialCameraPosition: CameraPosition(
target: LatLng(viewModel.locationLat, viewModel.locationLon),
zoom: 12,
),
),
),
);
}
}

View File

@@ -0,0 +1,113 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/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/features/shifts/domain/shift_entity.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:whatsapp_unilink/whatsapp_unilink.dart';
class ShiftManageWidget extends StatelessWidget {
final List<ShiftManager> managers;
const ShiftManageWidget({super.key, required this.managers});
@override
Widget build(BuildContext context) {
if (managers.isEmpty) return const SizedBox.shrink();
return Container(
padding: const EdgeInsets.all(12.0),
margin: const EdgeInsets.only(left: 16, right: 16, top: 8),
decoration: KwBoxDecorations.primaryLight12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'manage_contact_details'.tr().toUpperCase(),
style: AppTextStyles.captionReg
.copyWith(color: AppColors.blackCaptionText),
),
const Gap(16),
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: managers.length,
itemBuilder: (context, index) {
return _buildManager(managers[index]);
},
separatorBuilder: (context, index) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Divider(
color: AppColors.grayTintStroke,
),
);
},
),
],
),
);
}
Widget _buildManager(ShiftManager manager) {
return Row(
children: [
if(manager.imageUrl.isNotEmpty)
CircleAvatar(
radius: 24.0,
backgroundColor: AppColors.grayWhite,
backgroundImage: CachedNetworkImageProvider(
manager.imageUrl), // Replace with your image asset
),
const SizedBox(width: 16.0),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
manager.name,
style: AppTextStyles.bodyMediumSmb,
),
const SizedBox(height: 4.0),
Text(
manager.phoneNumber,
style: AppTextStyles.bodySmallReg
.copyWith(color: AppColors.blackGray),
),
],
),
),
const SizedBox(width: 8.0),
Row(
children: [
KwButton.outlinedPrimary(
rightIcon: Assets.images.icons.whatsapp,
fit: KwButtonFit.circular,
onPressed: () {
var link = WhatsAppUnilink(
phoneNumber: manager.phoneNumber,
text: '',
);
launchUrlString(link.toString());
},
),
const SizedBox(width: 8.0),
KwButton.outlinedPrimary(
label: 'call'.tr(),
rightIcon: Assets.images.icons.call,
onPressed: () {
launchUrlString('tel:${manager.phoneNumber}');
},
).copyWith(
color: AppColors.tintDarkGreen,
textColors: AppColors.statusSuccess),
],
)
],
);
}
}

View File

@@ -0,0 +1,33 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.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/shift_payment_step_widget.dart';
import 'package:krow/features/shifts/domain/shift_entity.dart';
class ShiftPaymentStepCardWidget extends StatelessWidget {
final ShiftEntity viewModel;
const ShiftPaymentStepCardWidget({super.key, required this.viewModel});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(12.0),
margin: const EdgeInsets.only(left: 16, right: 16, top: 8),
decoration: KwBoxDecorations.primaryLight12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'payment_status'.tr().toUpperCase(),
style: AppTextStyles.captionReg
.copyWith(color: AppColors.blackCaptionText),
),
const ShiftPaymentStepWidget(currentIndex: 1),
],
),
);
}
}

View File

@@ -0,0 +1,60 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/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/shifts/domain/shift_entity.dart';
class ShiftRatingWidget extends StatelessWidget {
final ShiftEntity viewModel;
const ShiftRatingWidget({super.key, required this.viewModel});
final double maxRating = 5.0; // Maximum rating, e.g., 5
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(12.0),
margin: const EdgeInsets.only(left: 16, right: 16, top: 8),
decoration: KwBoxDecorations.primaryLight12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'clients_rate'.tr().toUpperCase(),
style: AppTextStyles.captionReg
.copyWith(color: AppColors.blackCaptionText),
),
const Gap(4),
Center(
child: Text(viewModel.rating?.rating.toStringAsFixed(1) ?? '0.0',
style: AppTextStyles.headingH0),
),
const Gap(4),
_buildRating(viewModel.rating?.rating ?? 0),
],
),
);
}
_buildRating(double rating) {
List<Widget> stars = [];
for (int i = 1; i <= maxRating; i++) {
if (i <= rating) {
stars.add(Assets.images.icons.ratingStar.star.svg());
} else if (i - rating < 1) {
stars.add(Assets.images.icons.ratingStar.starHalf.svg());
} else {
stars.add(Assets.images.icons.ratingStar.starEmpty.svg());
}
}
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [const Gap(28), ...stars, const Gap(28)],
);
}
}

View File

@@ -0,0 +1,218 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/application/common/map_utils.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/ui_kit/dialogs/kw_dialog.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
import 'package:krow/features/shifts/data/models/staff_shift.dart';
import 'package:krow/features/shifts/domain/blocs/shift_deteils_bloc/shift_details_bloc.dart';
import 'package:krow/features/shifts/domain/services/shift_completer_service.dart';
import 'package:krow/features/shifts/domain/shift_entity.dart';
import 'package:krow/features/shifts/presentation/widgets/shift_timer_widgets/shift_timer_widget.dart';
class ShiftTimerCardWidget extends StatelessWidget {
const ShiftTimerCardWidget({super.key});
bool _hasBorder(EventShiftRoleStaffStatus status) =>
status == EventShiftRoleStaffStatus.ongoing;
@override
Widget build(BuildContext context) {
return BlocBuilder<ShiftDetailsBloc, ShiftDetailsState>(
buildWhen: (previous, current) =>
previous.isToFar != current.isToFar ||
previous.shiftViewModel != current.shiftViewModel,
builder: (context, state) {
return Container(
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.only(top: 8, left: 16, right: 16),
decoration: KwBoxDecorations.primaryLight12.copyWith(
border: _hasBorder(state.shiftViewModel.status)
? Border.all(color: AppColors.tintDarkGreen, width: 2)
: null,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'timer'.tr().toUpperCase(),
style: AppTextStyles.captionReg
.copyWith(color: AppColors.blackCaptionText),
),
const Gap(16),
ShiftTimerWidget(
shiftStatus: state.shiftViewModel.status,
clockIn: state.shiftViewModel.clockIn,
clockOut: state.shiftViewModel.clockOut,
),
_buildButton(
context,
state.isToFar,
state.shiftViewModel,
),
],
),
);
},
);
}
_buildButton(
context,
bool isToFar,
ShiftEntity viewModel,
) {
final status = viewModel.status;
return Column(
children: [
if (isToFar && status == EventShiftRoleStaffStatus.confirmed ||
status == EventShiftRoleStaffStatus.ongoing)
_toFarMessage(status),
const Gap(16),
KwButton.primary(
disabled: (status == EventShiftRoleStaffStatus.confirmed ||
status == EventShiftRoleStaffStatus.ongoing) &&
isToFar != false,
label: status == EventShiftRoleStaffStatus.confirmed
? 'clock_in'.tr()
: 'clock_out'.tr(),
onPressed: () async {
if (status == EventShiftRoleStaffStatus.confirmed) {
_onClockIn(context, viewModel.eventId);
} else {
await _onClocOut(context, viewModel.eventId, viewModel);
}
},
),
if (isToFar && status == EventShiftRoleStaffStatus.confirmed) ...[
const Gap(16),
KwButton.outlinedPrimary(
leftIcon: Assets.images.icons.routing,
label: 'get_direction'.tr(),
onPressed: () {
MapUtils.openMapByLatLon(
viewModel.locationLat,
viewModel.locationLon,
);
},
)
]
],
);
}
Widget _toFarMessage(status) {
return Container(
margin: const EdgeInsets.only(top: 16),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColors.graySecondaryFrame,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Container(
height: 28,
width: 28,
decoration: const BoxDecoration(
color: AppColors.grayWhite,
shape: BoxShape.circle,
),
child: Assets.images.icons.alertCircle.svg(
height: 12,
width: 12,
colorFilter:
const ColorFilter.mode(AppColors.blackBlack, BlendMode.srcIn),
),
),
const Gap(8),
Expanded(
child: Text(
status == EventShiftRoleStaffStatus.ongoing
? 'reach_location_to_clock_out'.tr()
: 'reach_location_to_clock_in'.tr(),
style: AppTextStyles.bodySmallMed,
),
),
],
),
);
}
Future<void> _onClocOut(
BuildContext context, String eventId, ShiftEntity shift) async {
if (!kDebugMode) {
var qrResult = await context.router.push(const QrScannerRoute());
if (!context.mounted) return;
if (qrResult == null || qrResult != eventId) {
KwDialog.show(
context: context,
icon: Assets.images.icons.alertTriangle,
state: KwDialogState.negative,
title: 'oops_something_wrong'.tr(),
message: 'qr_code_error'.tr(),
primaryButtonLabel: 'retry_scanning'.tr(),
secondaryButtonLabel: 'cancel'.tr(),
onPrimaryButtonPressed: (dialogContext) {
Navigator.pop(dialogContext);
_onClocOut(context, eventId, shift);
});
return;
}
}
ShiftCompleterService().startCompleteProcess(context, shift,
onComplete: () {
BlocProvider.of<ShiftDetailsBloc>(context)
.add(const ShiftCompleteEvent());
});
}
void _onClockIn(BuildContext context, String eventId) async {
if (kDebugMode) {
BlocProvider.of<ShiftDetailsBloc>(context).add(const ShiftClockInEvent());
return;
}
var result = await context.router.push(const QrScannerRoute());
if (!context.mounted) return;
if (result != null && result == eventId) {
KwDialog.show(
context: context,
icon: Assets.images.icons.like,
state: KwDialogState.positive,
title: 'youre_good_to_go'.tr(),
message: 'shift_timer_started'.tr(),
child: Text(
'lunch_break_reminder'.tr(),
textAlign: TextAlign.center,
style: AppTextStyles.bodyMediumMed,
),
primaryButtonLabel: 'Continue to Dashboard');
BlocProvider.of<ShiftDetailsBloc>(context).add(const ShiftClockInEvent());
} else {
KwDialog.show(
context: context,
icon: Assets.images.icons.alertTriangle,
state: KwDialogState.negative,
title: 'oops_something_wrong'.tr(),
message: 'qr_code_error'.tr(),
primaryButtonLabel: 'retry_scanning'.tr(),
secondaryButtonLabel: 'cancel'.tr(),
onPrimaryButtonPressed: (dialogContext) {
Navigator.pop(dialogContext);
_onClockIn(context, eventId);
});
}
}
}

View File

@@ -0,0 +1,121 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/features/shifts/data/models/staff_shift.dart';
class ShiftTimerWidget extends StatefulWidget {
final EventShiftRoleStaffStatus shiftStatus;
final DateTime? clockIn;
final DateTime? clockOut;
const ShiftTimerWidget(
{super.key,
required this.shiftStatus,
required this.clockIn,
required this.clockOut});
@override
State<ShiftTimerWidget> createState() => _ShiftTimerWidgetState();
}
class _ShiftTimerWidgetState extends State<ShiftTimerWidget> {
Color _borderColor() =>
widget.shiftStatus == EventShiftRoleStaffStatus.ongoing
? AppColors.tintDarkGreen
: AppColors.grayStroke;
Color _labelColor() => widget.shiftStatus == EventShiftRoleStaffStatus.ongoing
? AppColors.statusSuccess
: AppColors.blackGray;
Color _counterColor() =>
widget.shiftStatus == EventShiftRoleStaffStatus.ongoing
? AppColors.statusSuccess
: AppColors.blackBlack;
Color _bgColor() => widget.shiftStatus == EventShiftRoleStaffStatus.ongoing
? AppColors.tintGreen
: AppColors.graySecondaryFrame;
Color _dividerColor() =>
widget.shiftStatus == EventShiftRoleStaffStatus.ongoing
? AppColors.tintDarkGreen
: AppColors.grayTintStroke;
Duration _getDuration() {
return widget.shiftStatus == EventShiftRoleStaffStatus.ongoing
? DateTime.now()
.toUtc()
.difference(widget.clockIn ?? DateTime.now().toUtc())
: Duration.zero;
}
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
var duration = _getDuration();
var hours = duration.inHours.remainder(24).abs().toString().padLeft(2, '0');
var minutes =
duration.inMinutes.remainder(60).abs().toString().padLeft(2, '0');
var seconds =
duration.inSeconds.remainder(60).abs().toString().padLeft(2, '0');
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
height: 80,
decoration: BoxDecoration(
color: _bgColor(),
border: Border.all(color: _borderColor()),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildTimeText('hours'.tr(), hours),
_divider(),
_buildTimeText('minutes'.tr(), minutes),
_divider(),
_buildTimeText('seconds'.tr(), seconds),
],
),
),
],
);
}
Widget _divider() {
return SizedBox(
width: 24,
child: Center(
child: Container(
height: 24,
width: 1,
color: _dividerColor(),
),
),
);
}
Widget _buildTimeText(String label, String time) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(time,
style: AppTextStyles.headingH3.copyWith(color: _counterColor())),
const Gap(6),
Text(
label,
style: AppTextStyles.bodySmallReg.copyWith(color: _labelColor()),
),
],
);
}
}