feat: legacy mobile apps created
This commit is contained in:
@@ -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();
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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._();
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
]
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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()});
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
) ??
|
||||
[],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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))),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user