feat: Refactor code structure and optimize performance across multiple modules
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:krow/core/entity/staff_contact_entity.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
part 'assigned_staff_event.dart';
|
||||
part 'assigned_staff_state.dart';
|
||||
|
||||
class AssignedStaffBloc extends Bloc<AssignedStaffEvent, AssignedStaffState> {
|
||||
AssignedStaffBloc(
|
||||
{required List<StaffContact> staffContacts, required String department})
|
||||
: super(AssignedStaffState(
|
||||
staffContacts: staffContacts,
|
||||
department: department,
|
||||
)) {}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
part of 'assigned_staff_bloc.dart';
|
||||
|
||||
@immutable
|
||||
sealed class AssignedStaffEvent {}
|
||||
@@ -0,0 +1,23 @@
|
||||
part of 'assigned_staff_bloc.dart';
|
||||
|
||||
@immutable
|
||||
class AssignedStaffState {
|
||||
final List<StaffContact> staffContacts;
|
||||
final String department;
|
||||
final bool inLoading;
|
||||
|
||||
const AssignedStaffState({required this.staffContacts, required this.department, this.inLoading = false});
|
||||
|
||||
copyWith({
|
||||
List<StaffContact>? staffContacts,
|
||||
String? department,
|
||||
bool? canManualAssign,
|
||||
bool? inLoading,
|
||||
}) {
|
||||
return AssignedStaffState(
|
||||
staffContacts: staffContacts ?? this.staffContacts,
|
||||
department: department ?? this.department,
|
||||
inLoading: inLoading ?? this.inLoading,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/core/entity/staff_contact_entity.dart';
|
||||
import 'package:krow/core/presentation/widgets/assigned_staff_item_widget.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_app_bar.dart';
|
||||
import 'package:krow/features/assigned_staff_screen/domain/assigned_staff_bloc.dart';
|
||||
|
||||
@RoutePage()
|
||||
class AssignedStaffScreen extends StatelessWidget implements AutoRouteWrapper {
|
||||
final List<StaffContact> staffContacts;
|
||||
|
||||
final String department;
|
||||
|
||||
const AssignedStaffScreen(
|
||||
{super.key, required this.staffContacts, required this.department});
|
||||
|
||||
@override
|
||||
Widget wrappedRoute(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => AssignedStaffBloc(
|
||||
staffContacts: staffContacts, department: department),
|
||||
child: this);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<AssignedStaffBloc, AssignedStaffState>(
|
||||
builder: (context, state) {
|
||||
return Scaffold(
|
||||
appBar: KwAppBar(
|
||||
titleText: 'Assigned Staff',
|
||||
),
|
||||
body: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: staffContacts.length,
|
||||
itemBuilder: (context, index) {
|
||||
return AssignedStaffItemWidget(
|
||||
staffContact: staffContacts[index], department: department);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'package:graphql_flutter/graphql_flutter.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:krow/core/application/clients/api/api_client.dart';
|
||||
import 'package:krow/core/application/clients/api/api_exception.dart';
|
||||
import 'package:krow/features/clock_manual/data/clock_manual_gql.dart';
|
||||
|
||||
@singleton
|
||||
class ClockManualApiProvider {
|
||||
final ApiClient _client;
|
||||
|
||||
ClockManualApiProvider({required ApiClient client}) : _client = client;
|
||||
|
||||
Future<void> trackClientClockin(String positionStaffId) async {
|
||||
final QueryResult result = await _client.mutate(
|
||||
schema: trackClientClockinMutation,
|
||||
body: {'position_staff_id': positionStaffId},
|
||||
);
|
||||
if (result.hasException) {
|
||||
throw parseBackendError(result.exception!);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> trackClientClockout(String positionStaffId) async {
|
||||
final QueryResult result = await _client.mutate(
|
||||
schema: trackClientClockoutMutation,
|
||||
body: {'position_staff_id': positionStaffId},
|
||||
);
|
||||
if (result.hasException) {
|
||||
throw parseBackendError(result.exception!);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> cancelClockin(String positionStaffId) async {
|
||||
final QueryResult result = await _client.mutate(
|
||||
schema: cancelClockinMutation,
|
||||
body: {'position_staff_id': positionStaffId},
|
||||
);
|
||||
if (result.hasException) {
|
||||
throw parseBackendError(result.exception!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
const String trackClientClockinMutation = r'''
|
||||
mutation track_client_clockin($position_staff_id: ID!) {
|
||||
track_client_clockin(position_staff_id: $position_staff_id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String trackClientClockoutMutation = r'''
|
||||
mutation track_client_clockout($position_staff_id: ID!) {
|
||||
track_client_clockout(position_staff_id: $position_staff_id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String cancelClockinMutation = r'''
|
||||
mutation cancel_client_clockin($position_staff_id: ID!) {
|
||||
cancel_client_clockin(position_staff_id: $position_staff_id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
''';
|
||||
@@ -0,0 +1,43 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:krow/features/clock_manual/data/clock_manual_api_provider.dart';
|
||||
import 'package:krow/features/clock_manual/domain/clock_manual_repository.dart';
|
||||
|
||||
@Singleton(as: ClockManualRepository)
|
||||
class ClockManualRepositoryImpl implements ClockManualRepository {
|
||||
final ClockManualApiProvider apiProvider;
|
||||
|
||||
ClockManualRepositoryImpl({required this.apiProvider});
|
||||
|
||||
@override
|
||||
Future<void> trackClientClockin(String positionStaffId) async {
|
||||
try {
|
||||
await apiProvider.trackClientClockin(positionStaffId);
|
||||
} catch (exception) {
|
||||
debugPrint(exception.toString());
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> trackClientClockout(String positionStaffId) async {
|
||||
try {
|
||||
await apiProvider.trackClientClockout(positionStaffId);
|
||||
} catch (exception) {
|
||||
debugPrint(exception.toString());
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> cancelClockin(String positionStaffId) async{
|
||||
try {
|
||||
await apiProvider.cancelClockin(positionStaffId);
|
||||
} catch (exception) {
|
||||
debugPrint(exception.toString());
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:krow/core/application/clients/api/api_exception.dart';
|
||||
import 'package:krow/core/application/di/injectable.dart';
|
||||
import 'package:krow/core/data/models/staff/pivot.dart';
|
||||
import 'package:krow/core/entity/staff_contact_entity.dart';
|
||||
import 'package:krow/features/clock_manual/domain/clock_manual_repository.dart';
|
||||
|
||||
part 'clock_manual_event.dart';
|
||||
part 'clock_manual_state.dart';
|
||||
|
||||
class ClockManualBloc extends Bloc<ClockManualEvent, ClockManualState> {
|
||||
var staffContacts = <StaffContact>[];
|
||||
|
||||
ClockManualBloc({required this.staffContacts})
|
||||
: super(ClockManualState(
|
||||
timers: Map.fromEntries(
|
||||
staffContacts.map((e) => MapEntry(e.id, ValueNotifier<int>(0)))),
|
||||
ongoingStaffContacts: staffContacts
|
||||
.where((element) => element.status == PivotStatus.ongoing)
|
||||
.toList()
|
||||
..sort((a, b) => a.startAt.compareTo(b.startAt)),
|
||||
confirmedStaffContacts: staffContacts
|
||||
.where((element) => element.status == PivotStatus.confirmed)
|
||||
.toList()
|
||||
..sort((a, b) => a.startAt.compareTo(b.startAt)),
|
||||
)) {
|
||||
on<ChangeSelectedTabIndexClockManual>(_onChangeSelectedTabIndexStaffManual);
|
||||
on<ClockInManual>(_onClockInStaffManual);
|
||||
on<SortStaffContactsClockManual>(_sortStaffContactsClockManual);
|
||||
on<ClockOutManual>(_onClockOutStaff);
|
||||
on<CancelClockInManual>(_onCancelClockInStaffManual);
|
||||
}
|
||||
|
||||
void _onChangeSelectedTabIndexStaffManual(
|
||||
ChangeSelectedTabIndexClockManual event, Emitter<ClockManualState> emit) {
|
||||
emit(state.copyWith(selectedTabIndex: event.index));
|
||||
}
|
||||
|
||||
void _onClockInStaffManual(ClockInManual event, emit) async {
|
||||
emit(state.copyWith(inLoading: true));
|
||||
try {
|
||||
await getIt<ClockManualRepository>()
|
||||
.trackClientClockin(event.staffContact.id);
|
||||
|
||||
_startCountdownTimer(
|
||||
state.timers[event.staffContact.id]!, event.staffContact);
|
||||
} catch (e) {
|
||||
if (e is DisplayableException) {
|
||||
emit(state.copyWith(
|
||||
errorMessage: e.message,
|
||||
));
|
||||
}
|
||||
}
|
||||
emit(state.copyWith(inLoading: false));
|
||||
}
|
||||
|
||||
void _onClockOutStaff(ClockOutManual event, emit) async {
|
||||
emit(state.copyWith(inLoading: false));
|
||||
try {
|
||||
await getIt<ClockManualRepository>()
|
||||
.trackClientClockout(event.staffContact.id);
|
||||
event.staffContact.status = PivotStatus.completed;
|
||||
add(SortStaffContactsClockManual());
|
||||
} catch (e) {
|
||||
if (e is DisplayableException) {
|
||||
emit(state.copyWith(
|
||||
errorMessage: e.message,
|
||||
));
|
||||
}
|
||||
}
|
||||
emit(state.copyWith(inLoading: false));
|
||||
}
|
||||
|
||||
void _onCancelClockInStaffManual(CancelClockInManual event, emit) async {
|
||||
emit(state.copyWith(inLoading: true));
|
||||
try {
|
||||
await getIt<ClockManualRepository>().cancelClockin(event.staffContact.id);
|
||||
state.timers[event.staffContact.id]!.value = -1;
|
||||
event.staffContact.status = PivotStatus.confirmed;
|
||||
add(SortStaffContactsClockManual());
|
||||
} catch (e) {
|
||||
if (e is DisplayableException) {
|
||||
emit(state.copyWith(
|
||||
errorMessage: e.message,
|
||||
));
|
||||
}
|
||||
}
|
||||
emit(state.copyWith(inLoading: false));
|
||||
}
|
||||
|
||||
void _sortStaffContactsClockManual(SortStaffContactsClockManual event, emit) {
|
||||
emit(state.copyWith(
|
||||
ongoingStaffContacts: staffContacts
|
||||
.where((element) => element.status == PivotStatus.ongoing)
|
||||
.toList()
|
||||
..sort((a, b) => a.startAt.compareTo(b.startAt)),
|
||||
confirmedStaffContacts: staffContacts
|
||||
.where((element) => element.status == PivotStatus.confirmed)
|
||||
.toList()
|
||||
..sort((a, b) => a.startAt.compareTo(b.startAt)),
|
||||
));
|
||||
}
|
||||
|
||||
void _startCountdownTimer(ValueNotifier<int> cancelTimer, staffContact) {
|
||||
cancelTimer.value = 5;
|
||||
Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (cancelTimer.value > 0) {
|
||||
cancelTimer.value -= 1;
|
||||
} else if (cancelTimer.value == 0) {
|
||||
staffContact.status = PivotStatus.ongoing;
|
||||
add(SortStaffContactsClockManual());
|
||||
timer.cancel();
|
||||
} else {
|
||||
timer.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
part of 'clock_manual_bloc.dart';
|
||||
|
||||
@immutable
|
||||
sealed class ClockManualEvent {}
|
||||
|
||||
class ChangeSelectedTabIndexClockManual extends ClockManualEvent {
|
||||
final int index;
|
||||
|
||||
ChangeSelectedTabIndexClockManual(this.index);
|
||||
}
|
||||
|
||||
class ClockInManual extends ClockManualEvent {
|
||||
final StaffContact staffContact;
|
||||
|
||||
ClockInManual(this.staffContact);
|
||||
}
|
||||
|
||||
class SortStaffContactsClockManual extends ClockManualEvent {}
|
||||
|
||||
class ClockOutManual extends ClockManualEvent {
|
||||
final StaffContact staffContact;
|
||||
|
||||
ClockOutManual(this.staffContact);
|
||||
}
|
||||
|
||||
class CancelClockInManual extends ClockManualEvent {
|
||||
final StaffContact staffContact;
|
||||
|
||||
CancelClockInManual(this.staffContact);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
part of 'clock_manual_bloc.dart';
|
||||
|
||||
@immutable
|
||||
class ClockManualState {
|
||||
final List<StaffContact> confirmedStaffContacts;
|
||||
final List<StaffContact> ongoingStaffContacts;
|
||||
final int selectedTabIndex;
|
||||
final bool inLoading;
|
||||
final String? errorMessage;
|
||||
|
||||
final Map<String, ValueNotifier<int>> timers ;
|
||||
|
||||
const ClockManualState(
|
||||
{required this.confirmedStaffContacts,
|
||||
required this.ongoingStaffContacts,
|
||||
this.inLoading = false,
|
||||
this.errorMessage,
|
||||
this.timers = const {},
|
||||
this.selectedTabIndex = 0});
|
||||
|
||||
copyWith({
|
||||
List<StaffContact>? confirmedStaffContacts,
|
||||
List<StaffContact>? ongoingStaffContacts,
|
||||
bool? inLoading,
|
||||
String? errorMessage,
|
||||
int? selectedTabIndex,
|
||||
Map<String, ValueNotifier<int>>? timers,
|
||||
}) {
|
||||
return ClockManualState(
|
||||
timers: timers ?? this.timers,
|
||||
confirmedStaffContacts:
|
||||
confirmedStaffContacts ?? this.confirmedStaffContacts,
|
||||
ongoingStaffContacts: ongoingStaffContacts ?? this.ongoingStaffContacts,
|
||||
inLoading: inLoading ?? this.inLoading,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
selectedTabIndex: selectedTabIndex ?? this.selectedTabIndex,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
abstract class ClockManualRepository {
|
||||
Future<void> trackClientClockin(String positionStaffId);
|
||||
|
||||
Future<void> trackClientClockout(String positionStaffId);
|
||||
|
||||
Future<void> cancelClockin(String positionStaffId);
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
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/str_extensions.dart';
|
||||
import 'package:krow/core/data/models/staff/pivot.dart';
|
||||
import 'package:krow/core/entity/staff_contact_entity.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
|
||||
import 'package:krow/features/clock_manual/domain/bloc/clock_manual_bloc.dart';
|
||||
|
||||
class AssignedStaffManualItemWidget extends StatelessWidget {
|
||||
final StaffContact staffContact;
|
||||
|
||||
final ValueNotifier<int> timer;
|
||||
|
||||
const AssignedStaffManualItemWidget(
|
||||
{super.key, required this.staffContact, required this.timer});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: timer,
|
||||
builder: (BuildContext context, int value, Widget? child) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
decoration: KwBoxDecorations.white8,
|
||||
child: Column(
|
||||
children: [
|
||||
_staffInfo(),
|
||||
const Gap(12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(staffContact.status == PivotStatus.ongoing?'End Time': 'Start Time',
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.blackGray)),
|
||||
Text(
|
||||
DateFormat('hh:mm a').format(
|
||||
(staffContact.status == PivotStatus.ongoing
|
||||
? staffContact.parentPosition?.endTime
|
||||
: staffContact.parentPosition?.startTime) ??
|
||||
DateTime.now()),
|
||||
style: AppTextStyles.bodySmallMed,
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(12),
|
||||
if (staffContact.status == PivotStatus.confirmed && value <= 0)
|
||||
_buildClockIn(context),
|
||||
if (staffContact.status == PivotStatus.ongoing)
|
||||
_buildClockOut(context),
|
||||
if (value > 0) _buildCancel(context),
|
||||
if (kDebugMode) _buildCancel(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
KwButton _buildCancel(BuildContext context) {
|
||||
return KwButton.outlinedPrimary(
|
||||
onPressed: () {
|
||||
BlocProvider.of<ClockManualBloc>(context)
|
||||
.add(CancelClockInManual(staffContact));
|
||||
},
|
||||
label: 'Click again to cancel (${timer.value}) ',
|
||||
height: 36)
|
||||
.copyWith(
|
||||
textColors: AppColors.blackGray,
|
||||
color: AppColors.grayWhite,
|
||||
isFilledOutlined: true,
|
||||
borderColor: AppColors.grayTintStroke,
|
||||
pressedColor: AppColors.grayWhite);
|
||||
}
|
||||
|
||||
KwButton _buildClockIn(BuildContext context) {
|
||||
return KwButton.outlinedPrimary(
|
||||
onPressed: () {
|
||||
BlocProvider.of<ClockManualBloc>(context)
|
||||
.add(ClockInManual(staffContact));
|
||||
},
|
||||
label: 'Clock In',
|
||||
height: 36)
|
||||
.copyWith(
|
||||
textColors: AppColors.statusSuccess,
|
||||
color: AppColors.tintGreen,
|
||||
isFilledOutlined: true,
|
||||
borderColor: AppColors.tintDarkGreen,
|
||||
pressedColor: AppColors.tintDarkGreen);
|
||||
}
|
||||
|
||||
KwButton _buildClockOut(BuildContext context) {
|
||||
return KwButton.outlinedPrimary(
|
||||
onPressed: () {
|
||||
BlocProvider.of<ClockManualBloc>(context)
|
||||
.add(ClockOutManual(staffContact));
|
||||
},
|
||||
label: 'Clock Out',
|
||||
height: 36)
|
||||
.copyWith(
|
||||
textColors: AppColors.statusError,
|
||||
color: AppColors.tintRed,
|
||||
isFilledOutlined: true,
|
||||
borderColor: AppColors.tintDarkRed,
|
||||
pressedColor: AppColors.tintDarkRed);
|
||||
}
|
||||
|
||||
Row _staffInfo() {
|
||||
return Row(
|
||||
children: [
|
||||
if ((staffContact.photoUrl ?? '').isNotEmpty)
|
||||
CircleAvatar(
|
||||
radius: 18,
|
||||
backgroundImage: NetworkImage(staffContact.photoUrl ?? ''),
|
||||
),
|
||||
const Gap(12),
|
||||
Flexible(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${staffContact.firstName} ${staffContact.lastName}',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
staffContact.phoneNumber ?? '',
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
SizedBox(
|
||||
height: 40,
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Container(
|
||||
height: 20,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: staffContact.status.getStatusBorderColor(),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
padding: const EdgeInsets.all(0),
|
||||
decoration: BoxDecoration(
|
||||
color: staffContact.status.getStatusTextColor(),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
const Gap(2),
|
||||
Text(
|
||||
staffContact.status.formattedName.capitalize(),
|
||||
style: AppTextStyles.bodyTinyMed.copyWith(
|
||||
color: staffContact.status.getStatusTextColor()),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/core/entity/staff_contact_entity.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_app_bar.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_option_selector.dart';
|
||||
import 'package:krow/features/clock_manual/domain/bloc/clock_manual_bloc.dart';
|
||||
import 'package:krow/features/clock_manual/presentation/clock_manual_list_item.dart';
|
||||
|
||||
@RoutePage()
|
||||
class ClockManualScreen extends StatelessWidget implements AutoRouteWrapper {
|
||||
final List<StaffContact> staffContacts;
|
||||
|
||||
const ClockManualScreen({
|
||||
super.key,
|
||||
required this.staffContacts,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget wrappedRoute(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => ClockManualBloc(staffContacts: staffContacts),
|
||||
child: this);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<ClockManualBloc, ClockManualState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.errorMessage != current.errorMessage,
|
||||
listener: (context, state) {
|
||||
if (state.errorMessage != null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(state.errorMessage ?? ''),
|
||||
));
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
var items = state.selectedTabIndex == 0
|
||||
? state.confirmedStaffContacts
|
||||
: state.ongoingStaffContacts;
|
||||
return Scaffold(
|
||||
appBar: KwAppBar(
|
||||
titleText: 'Assigned Staff',
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, top: 16),
|
||||
child: KwOptionSelector(
|
||||
selectedIndex: state.selectedTabIndex,
|
||||
onChanged: (index) {
|
||||
BlocProvider.of<ClockManualBloc>(context)
|
||||
.add(ChangeSelectedTabIndexClockManual(index));
|
||||
},
|
||||
height: 26,
|
||||
selectorHeight: 4,
|
||||
textStyle: AppTextStyles.bodyMediumReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
selectedTextStyle: AppTextStyles.bodyMediumMed,
|
||||
itemAlign: Alignment.topCenter,
|
||||
items: const [
|
||||
'Clock In',
|
||||
'Clock Out',
|
||||
]),
|
||||
),
|
||||
ListView.builder(
|
||||
primary: false,
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
return AssignedStaffManualItemWidget(
|
||||
staffContact: items[index],
|
||||
timer: state.timers[items[index].id]!,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@RoutePage()
|
||||
class CreateEventFlowScreen extends StatelessWidget {
|
||||
const CreateEventFlowScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const AutoRouter();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import 'package:graphql/client.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:krow/core/application/clients/api/api_client.dart';
|
||||
import 'package:krow/core/data/models/event/addon_model.dart';
|
||||
import 'package:krow/core/data/models/event/business_member_model.dart';
|
||||
import 'package:krow/core/data/models/event/hub_model.dart';
|
||||
import 'package:krow/core/data/models/event/tag_model.dart';
|
||||
import 'package:krow/core/data/models/shift/business_skill_model.dart';
|
||||
import 'package:krow/core/data/models/shift/department_model.dart';
|
||||
import 'package:krow/features/create_event/data/create_event_gql.dart';
|
||||
|
||||
@Injectable()
|
||||
class CreateEventApiProvider {
|
||||
final ApiClient _client;
|
||||
|
||||
CreateEventApiProvider({required ApiClient client}) : _client = client;
|
||||
|
||||
Future<List<HubModel>> getHubs() async {
|
||||
QueryResult result = await _client.query(
|
||||
schema: getClientHubsQuery,
|
||||
);
|
||||
if (result.hasException) {
|
||||
throw Exception(result.exception.toString());
|
||||
}
|
||||
|
||||
if (result.data == null || result.data!['client_hubs'] == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (result.data!['client_hubs'] as List)
|
||||
.map((e) => HubModel.fromJson(e))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<List<AddonModel>> getAddons() async {
|
||||
QueryResult result = await _client.query(
|
||||
schema: getClientAddonsQuery,
|
||||
);
|
||||
if (result.hasException) {
|
||||
throw Exception(result.exception.toString());
|
||||
}
|
||||
|
||||
if (result.data == null || result.data!['client_business_addons'] == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (result.data!['client_business_addons'] as List)
|
||||
.map((e) => AddonModel.fromJson(e))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<List<TagModel>> getTags() async {
|
||||
QueryResult result = await _client.query(
|
||||
schema: getClientTagsQuery,
|
||||
);
|
||||
if (result.hasException) {
|
||||
throw Exception(result.exception.toString());
|
||||
}
|
||||
|
||||
if (result.data == null || result.data!['client_tags'] == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (result.data!['client_tags'] as List)
|
||||
.map((e) => TagModel.fromJson(e))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<List<BusinessMemberModel>> getContacts() async {
|
||||
QueryResult result = await _client.query(
|
||||
schema: getClientMembersQuery,
|
||||
);
|
||||
if (result.hasException) {
|
||||
throw Exception(result.exception.toString());
|
||||
}
|
||||
|
||||
if (result.data == null || result.data!['client_shift_contacts'] == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (result.data!['client_shift_contacts'] as List)
|
||||
.map((e) => BusinessMemberModel.fromJson(e))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<List<BusinessSkillModel>> getSkill() async {
|
||||
QueryResult result = await _client.query(
|
||||
schema: getClientBusinessSkillQuery,
|
||||
);
|
||||
if (result.hasException) {
|
||||
throw Exception(result.exception.toString());
|
||||
}
|
||||
|
||||
if (result.data == null || result.data!['client_business_skills'] == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (result.data!['client_business_skills'] as List)
|
||||
.map((e) => BusinessSkillModel.fromJson(e))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<List<DepartmentModel>> getDepartments(String hubId) async {
|
||||
QueryResult result = await _client.query(
|
||||
schema: getClientDepartmentsQuery,
|
||||
body: {'hub_id': hubId},
|
||||
);
|
||||
if (result.hasException) {
|
||||
throw Exception(result.exception.toString());
|
||||
}
|
||||
if (result.data == null || result.data!['client_hub_departments'] == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (result.data!['client_hub_departments'] as List)
|
||||
.map((e) => DepartmentModel.fromJson(e))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
const String getClientHubsQuery = r'''
|
||||
query getClientHubs() {
|
||||
client_hubs {
|
||||
id
|
||||
name
|
||||
address
|
||||
full_address {
|
||||
street_number
|
||||
zip_code
|
||||
latitude
|
||||
longitude
|
||||
formatted_address
|
||||
street
|
||||
region
|
||||
city
|
||||
country
|
||||
}
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String getClientAddonsQuery = r'''
|
||||
query getClientAddons() {
|
||||
client_business_addons {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String getClientTagsQuery = r'''
|
||||
query getClientTags() {
|
||||
client_tags {
|
||||
id
|
||||
name
|
||||
slug
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String getClientBusinessSkillQuery = r'''
|
||||
query getClientSkills() {
|
||||
client_business_skills {
|
||||
id
|
||||
skill {
|
||||
id
|
||||
name
|
||||
slug
|
||||
price
|
||||
}
|
||||
price
|
||||
is_active
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String getClientMembersQuery = r'''
|
||||
query getClientMembers() {
|
||||
client_shift_contacts() {
|
||||
id
|
||||
first_name
|
||||
last_name
|
||||
title
|
||||
avatar
|
||||
auth_info {
|
||||
email
|
||||
phone
|
||||
}
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String getClientDepartmentsQuery = r'''
|
||||
query getClientSkills($hub_id: ID!) {
|
||||
client_hub_departments(hub_id: $hub_id) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:krow/core/data/models/event/addon_model.dart';
|
||||
import 'package:krow/core/data/models/event/business_member_model.dart';
|
||||
import 'package:krow/core/data/models/event/hub_model.dart';
|
||||
import 'package:krow/core/data/models/event/tag_model.dart';
|
||||
import 'package:krow/core/data/models/shift/business_skill_model.dart';
|
||||
import 'package:krow/core/data/models/shift/department_model.dart';
|
||||
import 'package:krow/features/create_event/data/create_event_api_provider.dart';
|
||||
import 'package:krow/features/create_event/domain/create_event_repository.dart';
|
||||
|
||||
@Singleton(as: CreateEventRepository)
|
||||
class CreateEventRepositoryImpl extends CreateEventRepository {
|
||||
final CreateEventApiProvider _apiProvider;
|
||||
|
||||
CreateEventRepositoryImpl({required CreateEventApiProvider apiProvider})
|
||||
: _apiProvider = apiProvider;
|
||||
|
||||
@override
|
||||
Future<List<HubModel>> getHubs() async {
|
||||
return _apiProvider.getHubs();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<AddonModel>> getAddons() async {
|
||||
return _apiProvider.getAddons();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<TagModel>> getTags() async {
|
||||
return _apiProvider.getTags();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<BusinessMemberModel>> getContacts() async {
|
||||
return _apiProvider.getContacts();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<BusinessSkillModel>> getSkills() {
|
||||
return _apiProvider.getSkill();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<DepartmentModel>> getDepartments(String hubId) {
|
||||
return _apiProvider.getDepartments(hubId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/core/application/di/injectable.dart';
|
||||
import 'package:krow/core/data/models/event/addon_model.dart';
|
||||
import 'package:krow/core/data/models/event/business_member_model.dart';
|
||||
import 'package:krow/core/data/models/event/event_model.dart';
|
||||
import 'package:krow/core/data/models/event/hub_model.dart';
|
||||
import 'package:krow/core/data/models/event/tag_model.dart';
|
||||
import 'package:krow/core/data/models/shift/business_skill_model.dart';
|
||||
import 'package:krow/core/data/models/shift/department_model.dart';
|
||||
import 'package:krow/core/entity/event_entity.dart';
|
||||
import 'package:krow/core/entity/shift_entity.dart';
|
||||
import 'package:krow/core/sevices/create_event_service/create_event_service.dart';
|
||||
import 'package:krow/features/create_event/domain/create_event_repository.dart';
|
||||
import 'package:krow/features/create_event/domain/input_validator.dart';
|
||||
import 'package:krow/features/create_event/presentation/create_shift_details_section/bloc/create_shift_details_bloc.dart';
|
||||
|
||||
part 'create_event_event.dart';
|
||||
part 'create_event_state.dart';
|
||||
|
||||
class CreateEventBloc extends Bloc<CreateEventEvent, CreateEventState> {
|
||||
CreateEventBloc() : super(CreateEventState(entity: EventEntity.empty())) {
|
||||
on<CreateEventInit>(_onInit);
|
||||
// on<CreateEventChangeContractType>(_onChangeContractType);
|
||||
on<CreateEventChangeHub>(_onChangeHub);
|
||||
// on<CreateEventChangeContractNumber>(_onChangeContractNumber);
|
||||
on<CreateEventChangePoNumber>(_onChangePoNumber);
|
||||
on<CreateEventTagSelected>(_onTagSelected);
|
||||
on<CreateEventAddShift>(_onAddShift);
|
||||
on<CreateEventRemoveShift>(_onRemoveShift);
|
||||
on<CreateEventAddInfoChange>(_onAddInfoChange);
|
||||
on<CreateEventNameChange>(_onNameChange);
|
||||
on<CreateEventToggleAddon>(_onToggleAddon);
|
||||
on<CreateEventEntityUpdatedEvent>(_onEntityUpdated);
|
||||
on<CreateEventValidateAndPreview>(_onValidateAndPreview);
|
||||
on<DeleteDraftEvent>(_onDeleteDraft);
|
||||
}
|
||||
|
||||
Future<void> _onInit(
|
||||
CreateEventInit event, Emitter<CreateEventState> emit) async {
|
||||
emit(state.copyWith(inLoading: true));
|
||||
late EventEntity entity;
|
||||
bool? isEdit = event.eventModel != null;
|
||||
if (isEdit) {
|
||||
entity = EventEntity.fromEventDto(event.eventModel!);
|
||||
} else {
|
||||
entity = EventEntity.empty();
|
||||
}
|
||||
List<ShiftViewModel> shiftViewModels = [
|
||||
...entity.shifts?.map((shiftEntity) {
|
||||
return ShiftViewModel(
|
||||
id: shiftEntity.id,
|
||||
bloc: CreateShiftDetailsBloc(expanded: !isEdit)
|
||||
..add(CreateShiftInitializeEvent(shiftEntity)),
|
||||
);
|
||||
}).toList() ??
|
||||
[],
|
||||
];
|
||||
|
||||
emit(state.copyWith(
|
||||
recurringType: event.recurringType,
|
||||
entity: entity,
|
||||
tags: [
|
||||
//placeholder for tags - prevent UI jumping
|
||||
TagModel(id: '1', name: ' '),
|
||||
TagModel(id: '2', name: ' '),
|
||||
TagModel(id: '3', name: ' ')
|
||||
],
|
||||
shifts: shiftViewModels));
|
||||
List<Object> results;
|
||||
try {
|
||||
results = await Future.wait([
|
||||
getIt<CreateEventRepository>().getHubs().onError((e, s) {
|
||||
return [];
|
||||
}),
|
||||
getIt<CreateEventRepository>().getAddons().onError((e, s) => []),
|
||||
getIt<CreateEventRepository>().getTags().onError((e, s) => []),
|
||||
getIt<CreateEventRepository>().getContacts().onError((e, s) => []),
|
||||
getIt<CreateEventRepository>().getSkills().onError((e, s) => []),
|
||||
]);
|
||||
} catch (e) {
|
||||
emit(state.copyWith(inLoading: false));
|
||||
return;
|
||||
}
|
||||
var hubs = results[0] as List<HubModel>;
|
||||
var addons = results[1] as List<AddonModel>;
|
||||
var tags = results[2] as List<TagModel>;
|
||||
var contacts = results[3] as List<BusinessMemberModel>;
|
||||
var skills = results[4] as List<BusinessSkillModel>;
|
||||
|
||||
emit(state.copyWith(
|
||||
inLoading: false,
|
||||
hubs: hubs,
|
||||
tags: tags,
|
||||
addons: addons,
|
||||
contacts: contacts,
|
||||
skills: skills));
|
||||
}
|
||||
|
||||
void _onChangeHub(
|
||||
CreateEventChangeHub event, Emitter<CreateEventState> emit) async {
|
||||
if (event.hub == state.entity.hub) return;
|
||||
emit(state.copyWith(
|
||||
entity: state.entity.copyWith(hub: event.hub),
|
||||
));
|
||||
|
||||
state.shifts.forEach((element) {
|
||||
element.bloc
|
||||
.add(CreateShiftAddressSelectEvent(address: event.hub.fullAddress));
|
||||
});
|
||||
|
||||
var departments =
|
||||
await getIt<CreateEventRepository>().getDepartments(event.hub.id);
|
||||
|
||||
emit(state.copyWith(
|
||||
departments: departments,
|
||||
));
|
||||
}
|
||||
|
||||
// void _onChangeContractType(
|
||||
// CreateEventChangeContractType event, Emitter<CreateEventState> emit) {
|
||||
// if (event.contractType == state.entity.contractType) return;
|
||||
// emit(state.copyWith(
|
||||
// entity: state.entity.copyWith(contractType: event.contractType),
|
||||
// ));
|
||||
// }
|
||||
|
||||
// void _onChangeContractNumber(
|
||||
// CreateEventChangeContractNumber event, Emitter<CreateEventState> emit) {
|
||||
// emit(state.copyWith(
|
||||
// entity: state.entity.copyWith(contractNumber: event.contractNumber),
|
||||
// ));
|
||||
// }
|
||||
//
|
||||
void _onChangePoNumber(
|
||||
CreateEventChangePoNumber event, Emitter<CreateEventState> emit) {
|
||||
emit(state.copyWith(
|
||||
entity: state.entity.copyWith(poNumber: event.poNumber),
|
||||
));
|
||||
}
|
||||
|
||||
void _onTagSelected(
|
||||
CreateEventTagSelected event, Emitter<CreateEventState> emit) {
|
||||
final tags = List<TagModel>.of(state.entity.tags ?? []);
|
||||
if (tags.any((e) => e.id == event.tag.id)) {
|
||||
tags.removeWhere((e) => e.id == event.tag.id);
|
||||
} else {
|
||||
tags.add(event.tag);
|
||||
}
|
||||
emit(state.copyWith(
|
||||
entity: state.entity.copyWith(tags: tags),
|
||||
));
|
||||
}
|
||||
|
||||
void _onAddShift(CreateEventAddShift event, Emitter<CreateEventState> emit) {
|
||||
final id = DateTime.now().millisecondsSinceEpoch.toString();
|
||||
ShiftEntity newShiftEntity = ShiftEntity.empty();
|
||||
|
||||
final bloc = CreateShiftDetailsBloc(expanded: true)
|
||||
..add(CreateShiftInitializeEvent(
|
||||
newShiftEntity,
|
||||
));
|
||||
|
||||
newShiftEntity.parentEvent = state.entity;
|
||||
state.entity.shifts?.add(newShiftEntity);
|
||||
|
||||
emit(state.copyWith(
|
||||
shifts: [
|
||||
...state.shifts,
|
||||
ShiftViewModel(id: id, bloc: bloc),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
void _onRemoveShift(
|
||||
CreateEventRemoveShift event, Emitter<CreateEventState> emit) {
|
||||
emit(state.copyWith(
|
||||
entity: state.entity.copyWith(
|
||||
shifts: state.entity.shifts
|
||||
?.where((element) => element.id != event.id)
|
||||
.toList()),
|
||||
shifts: state.shifts.where((element) => element.id != event.id).toList(),
|
||||
));
|
||||
}
|
||||
|
||||
void _onNameChange(
|
||||
CreateEventNameChange event, Emitter<CreateEventState> emit) {
|
||||
emit(state.copyWith(
|
||||
entity: state.entity.copyWith(name: event.value),
|
||||
));
|
||||
}
|
||||
|
||||
void _onAddInfoChange(
|
||||
CreateEventAddInfoChange event, Emitter<CreateEventState> emit) {
|
||||
emit(state.copyWith(
|
||||
entity: state.entity.copyWith(additionalInfo: event.value),
|
||||
));
|
||||
}
|
||||
|
||||
void _onToggleAddon(
|
||||
CreateEventToggleAddon event, Emitter<CreateEventState> emit) {
|
||||
final addons = List<AddonModel>.of(state.entity.addons ?? []);
|
||||
if (addons.any((e) => e.id == event.addon.id)) {
|
||||
addons.removeWhere((e) => e.id == event.addon.id);
|
||||
} else {
|
||||
addons.add(event.addon);
|
||||
}
|
||||
emit(state.copyWith(
|
||||
entity: state.entity.copyWith(addons: addons),
|
||||
));
|
||||
}
|
||||
|
||||
void _onEntityUpdated(
|
||||
CreateEventEntityUpdatedEvent event, Emitter<CreateEventState> emit) {
|
||||
emit(state.copyWith());
|
||||
}
|
||||
|
||||
void _onValidateAndPreview(
|
||||
CreateEventValidateAndPreview event, Emitter<CreateEventState> emit) {
|
||||
var newState = CreateEventInputValidator.validateInputs(
|
||||
state.copyWith(entity: state.entity.copyWith()));
|
||||
emit(newState);
|
||||
emit(newState.copyWith(valid: false));
|
||||
}
|
||||
|
||||
void _onDeleteDraft(
|
||||
DeleteDraftEvent event, Emitter<CreateEventState> emit) async {
|
||||
await getIt<CreateEventService>().deleteDraft(state.entity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
part of 'create_event_bloc.dart';
|
||||
|
||||
@immutable
|
||||
sealed class CreateEventEvent {}
|
||||
|
||||
class CreateEventInit extends CreateEventEvent {
|
||||
final EventModel? eventModel;
|
||||
final EventScheduleType? recurringType;
|
||||
|
||||
CreateEventInit(this.recurringType, this.eventModel);
|
||||
}
|
||||
|
||||
// class CreateEventChangeContractType extends CreateEventEvent {
|
||||
// final EventContractType contractType;
|
||||
//
|
||||
// CreateEventChangeContractType(this.contractType);
|
||||
// }
|
||||
|
||||
class CreateEventChangeHub extends CreateEventEvent {
|
||||
final HubModel hub;
|
||||
|
||||
CreateEventChangeHub(this.hub);
|
||||
}
|
||||
|
||||
class CreateEventValidateAndPreview extends CreateEventEvent {
|
||||
CreateEventValidateAndPreview();
|
||||
}
|
||||
|
||||
// class CreateEventChangeContractNumber extends CreateEventEvent {
|
||||
// final String contractNumber;
|
||||
//
|
||||
// CreateEventChangeContractNumber(this.contractNumber);
|
||||
// }
|
||||
|
||||
class CreateEventChangePoNumber extends CreateEventEvent {
|
||||
final String poNumber;
|
||||
|
||||
CreateEventChangePoNumber(this.poNumber);
|
||||
}
|
||||
|
||||
class CreateEventNameChange extends CreateEventEvent {
|
||||
final String value;
|
||||
|
||||
CreateEventNameChange(this.value);
|
||||
}
|
||||
|
||||
class CreateEventAddInfoChange extends CreateEventEvent {
|
||||
final String value;
|
||||
|
||||
CreateEventAddInfoChange(this.value);
|
||||
}
|
||||
|
||||
class CreateEventTagSelected extends CreateEventEvent {
|
||||
final TagModel tag;
|
||||
|
||||
CreateEventTagSelected(this.tag);
|
||||
}
|
||||
|
||||
class CreateEventAddShift extends CreateEventEvent {
|
||||
CreateEventAddShift();
|
||||
}
|
||||
|
||||
class CreateEventRemoveShift extends CreateEventEvent {
|
||||
final String id;
|
||||
|
||||
CreateEventRemoveShift(this.id);
|
||||
}
|
||||
|
||||
class CreateEventToggleAddon extends CreateEventEvent {
|
||||
final AddonModel addon;
|
||||
|
||||
CreateEventToggleAddon(this.addon);
|
||||
}
|
||||
|
||||
class CreateEventEntityUpdatedEvent extends CreateEventEvent {
|
||||
CreateEventEntityUpdatedEvent();
|
||||
}
|
||||
|
||||
class DeleteDraftEvent extends CreateEventEvent {
|
||||
DeleteDraftEvent();
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
part of 'create_event_bloc.dart';
|
||||
|
||||
@immutable
|
||||
class CreateEventState {
|
||||
final bool inLoading;
|
||||
final bool success;
|
||||
final bool valid;
|
||||
final EventEntity entity;
|
||||
final EventScheduleType recurringType;
|
||||
final List<HubModel> hubs;
|
||||
final List<TagModel> tags;
|
||||
final List<BusinessMemberModel> contacts;
|
||||
final List<ShiftViewModel> shifts;
|
||||
final List<AddonModel> addons;
|
||||
final List<BusinessSkillModel> skills;
|
||||
final List<DepartmentModel> departments;
|
||||
final EventValidationState? validationState;
|
||||
|
||||
final bool showEmptyFieldError;
|
||||
|
||||
const CreateEventState(
|
||||
{required this.entity,
|
||||
this.inLoading = false,
|
||||
this.valid = false,
|
||||
this.showEmptyFieldError = false,
|
||||
this.success = false,
|
||||
this.recurringType = EventScheduleType.oneTime,
|
||||
this.hubs = const [],
|
||||
this.tags = const [],
|
||||
this.contacts = const [],
|
||||
this.addons = const [],
|
||||
this.skills = const [],
|
||||
this.shifts = const [],
|
||||
this.departments = const [],
|
||||
this.validationState});
|
||||
|
||||
CreateEventState copyWith({
|
||||
bool? inLoading,
|
||||
bool? success,
|
||||
bool? valid,
|
||||
bool? showEmptyFieldError,
|
||||
EventEntity? entity,
|
||||
EventScheduleType? recurringType,
|
||||
List<HubModel>? hubs,
|
||||
List<TagModel>? tags,
|
||||
List<BusinessMemberModel>? contacts,
|
||||
List<ShiftViewModel>? shifts,
|
||||
List<AddonModel>? addons,
|
||||
List<BusinessSkillModel>? skills,
|
||||
List<DepartmentModel>? departments,
|
||||
EventValidationState? validationState,
|
||||
}) {
|
||||
return CreateEventState(
|
||||
success: success ?? this.success,
|
||||
valid: valid ?? this.valid,
|
||||
inLoading: inLoading ?? this.inLoading,
|
||||
showEmptyFieldError: showEmptyFieldError ?? this.showEmptyFieldError,
|
||||
entity: entity ?? this.entity,
|
||||
recurringType: recurringType ?? this.recurringType,
|
||||
hubs: hubs ?? this.hubs,
|
||||
tags: tags ?? this.tags,
|
||||
contacts: contacts ?? this.contacts,
|
||||
shifts: shifts ?? this.shifts,
|
||||
addons: addons ?? this.addons,
|
||||
skills: skills ?? this.skills,
|
||||
departments: departments ?? this.departments,
|
||||
validationState: validationState,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ShiftViewModel {
|
||||
final String id;
|
||||
final CreateShiftDetailsBloc bloc;
|
||||
|
||||
ShiftViewModel({required this.id, required this.bloc});
|
||||
}
|
||||
|
||||
class EventValidationState {
|
||||
final String? nameError;
|
||||
final String? startDateError;
|
||||
final String? endDateError;
|
||||
final String? hubError;
|
||||
// final String? contractNumberError;
|
||||
final String? poNumberError;
|
||||
final String? shiftsError;
|
||||
|
||||
bool showed = false;
|
||||
|
||||
bool get hasError =>
|
||||
nameError != null ||
|
||||
startDateError != null ||
|
||||
endDateError != null ||
|
||||
hubError != null ||
|
||||
// contractNumberError != null ||
|
||||
poNumberError != null ||
|
||||
shiftsError != null;
|
||||
|
||||
String? get message {
|
||||
return nameError ??
|
||||
startDateError ??
|
||||
endDateError ??
|
||||
hubError ??
|
||||
// contractNumberError ??
|
||||
poNumberError ??
|
||||
shiftsError ??
|
||||
'';
|
||||
}
|
||||
|
||||
EventValidationState(
|
||||
{this.nameError,
|
||||
this.startDateError,
|
||||
this.endDateError,
|
||||
this.hubError,
|
||||
// this.contractNumberError,
|
||||
this.poNumberError,
|
||||
this.shiftsError});
|
||||
|
||||
EventValidationState copyWith({
|
||||
String? nameError,
|
||||
String? startDateError,
|
||||
String? endDateError,
|
||||
String? hubError,
|
||||
// String? contractNumberError,
|
||||
String? poNumberError,
|
||||
String? shiftsError,
|
||||
}) {
|
||||
return EventValidationState(
|
||||
nameError: nameError ?? this.nameError,
|
||||
startDateError: startDateError ?? this.startDateError,
|
||||
endDateError: endDateError ?? this.endDateError,
|
||||
hubError: hubError ?? this.hubError,
|
||||
// contractNumberError: contractNumberError ?? this.contractNumberError,
|
||||
poNumberError: poNumberError ?? this.poNumberError,
|
||||
shiftsError: shiftsError ?? this.shiftsError,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import 'package:krow/core/data/models/event/addon_model.dart';
|
||||
import 'package:krow/core/data/models/event/business_member_model.dart';
|
||||
import 'package:krow/core/data/models/event/hub_model.dart';
|
||||
import 'package:krow/core/data/models/event/tag_model.dart';
|
||||
import 'package:krow/core/data/models/shift/business_skill_model.dart';
|
||||
import 'package:krow/core/data/models/shift/department_model.dart';
|
||||
|
||||
abstract class CreateEventRepository {
|
||||
Future<List<HubModel>> getHubs();
|
||||
|
||||
Future<List<AddonModel>> getAddons();
|
||||
|
||||
Future<List<TagModel>> getTags();
|
||||
|
||||
Future<List<BusinessMemberModel>> getContacts();
|
||||
|
||||
Future<List<BusinessSkillModel>> getSkills();
|
||||
|
||||
Future<List<DepartmentModel>> getDepartments(String hubId);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
@singleton
|
||||
class GooglePlacesService {
|
||||
Future<List<MapPlace>> fetchSuggestions(String query) async {
|
||||
final String apiKey = dotenv.env['GOOGLE_MAP']!;
|
||||
|
||||
const String baseUrl =
|
||||
'https://maps.googleapis.com/maps/api/place/autocomplete/json';
|
||||
final Uri uri =
|
||||
Uri.parse('$baseUrl?input=$query&key=$apiKey&types=geocode');
|
||||
|
||||
final response = await http.get(uri);
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
final List<dynamic> predictions = data['predictions'];
|
||||
|
||||
return predictions.map((prediction) {
|
||||
return MapPlace.fromJson(prediction);
|
||||
}).toList();
|
||||
} else {
|
||||
throw Exception('Failed to fetch place suggestions');
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getPlaceDetails(String placeId) async {
|
||||
final String apiKey = dotenv.env['GOOGLE_MAP']!;
|
||||
final String url =
|
||||
'https://maps.googleapis.com/maps/api/place/details/json?place_id=$placeId&key=$apiKey';
|
||||
|
||||
final response = await http.get(Uri.parse(url));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
final result = data['result'];
|
||||
final location = result['geometry']['location'];
|
||||
|
||||
Map<String, dynamic> addressDetails = {
|
||||
'lat': location['lat'], // Latitude
|
||||
'lng': location['lng'], // Longitude
|
||||
'formatted_address': result['formatted_address'], // Full Address
|
||||
};
|
||||
|
||||
for (var component in result['address_components']) {
|
||||
List types = component['types'];
|
||||
if (types.contains('street_number')) {
|
||||
addressDetails['street_number'] = component['long_name'];
|
||||
}
|
||||
if (types.contains('route')) {
|
||||
addressDetails['street'] = component['long_name'];
|
||||
}
|
||||
if (types.contains('locality')) {
|
||||
addressDetails['city'] = component['long_name'];
|
||||
}
|
||||
if (types.contains('administrative_area_level_1')) {
|
||||
addressDetails['state'] = component['long_name'];
|
||||
}
|
||||
if (types.contains('country')) {
|
||||
addressDetails['country'] = component['long_name'];
|
||||
}
|
||||
if (types.contains('postal_code')) {
|
||||
addressDetails['postal_code'] = component['long_name'];
|
||||
}
|
||||
}
|
||||
|
||||
return addressDetails;
|
||||
} else {
|
||||
throw Exception('Failed to fetch place details');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MapPlace {
|
||||
final String description;
|
||||
final String placeId;
|
||||
|
||||
MapPlace({required this.description, required this.placeId});
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
'description': description,
|
||||
'place_id': placeId,
|
||||
};
|
||||
}
|
||||
|
||||
factory MapPlace.fromJson(Map<String, dynamic> json) {
|
||||
return MapPlace(
|
||||
description: json['description'],
|
||||
placeId: json['place_id'],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import 'package:krow/core/data/models/event/event_model.dart';
|
||||
import 'package:krow/features/create_event/domain/bloc/create_event_bloc.dart';
|
||||
import 'package:krow/features/create_event/presentation/create_role_section/bloc/create_role_bloc.dart';
|
||||
import 'package:krow/features/create_event/presentation/create_shift_details_section/bloc/create_shift_details_bloc.dart';
|
||||
|
||||
class CreateEventInputValidator {
|
||||
static CreateEventState validateInputs(CreateEventState state) {
|
||||
var newState = _validateEvent(state);
|
||||
newState = _validateShifts(newState);
|
||||
return newState;
|
||||
}
|
||||
|
||||
static CreateEventState _validateEvent(CreateEventState state) {
|
||||
EventValidationState? validationState;
|
||||
if (state.entity.name.isEmpty) {
|
||||
validationState = EventValidationState(nameError: 'Name cannot be empty');
|
||||
}
|
||||
|
||||
if (state.entity.startDate == null) {
|
||||
validationState =
|
||||
EventValidationState(startDateError: 'Start Date cannot be empty');
|
||||
}
|
||||
|
||||
if (state.recurringType == EventScheduleType.recurring &&
|
||||
state.entity.endDate == null) {
|
||||
validationState =
|
||||
EventValidationState(endDateError: 'End Date cannot be empty');
|
||||
}
|
||||
if (state.entity.hub == null) {
|
||||
validationState = EventValidationState(hubError: 'Hub cannot be empty');
|
||||
}
|
||||
// if (state.entity.contractType == EventContractType.contract &&
|
||||
// (state.entity.contractNumber?.isEmpty ?? true)) {
|
||||
// validationState = EventValidationState(
|
||||
// contractNumberError: 'Contract Number cannot be empty');
|
||||
// }
|
||||
if ((state.entity.poNumber?.isEmpty ?? true)) {
|
||||
validationState =
|
||||
EventValidationState(poNumberError: 'PO Number cannot be empty');
|
||||
}
|
||||
|
||||
if (state.shifts.isEmpty) {
|
||||
validationState =
|
||||
EventValidationState(shiftsError: 'Shifts cannot be empty');
|
||||
}
|
||||
|
||||
if (state.validationState == null && validationState == null) {
|
||||
return state.copyWith(valid: true);
|
||||
} else {
|
||||
return state.copyWith(validationState: validationState);
|
||||
}
|
||||
}
|
||||
|
||||
static CreateEventState _validateShifts(CreateEventState state) {
|
||||
for (var shift in state.shifts) {
|
||||
ShiftValidationState? validationState;
|
||||
if (!(shift.bloc.state.shift.fullAddress?.isValid() ?? false)) {
|
||||
validationState = ShiftValidationState(addressError: 'Invalid Address');
|
||||
}
|
||||
|
||||
if (shift.bloc.state.shift.managers.isEmpty) {
|
||||
validationState =
|
||||
ShiftValidationState(contactsError: 'Managers cannot be empty');
|
||||
}
|
||||
|
||||
if (validationState != null) {
|
||||
shift.bloc.add(ValidationFailedEvent(validationState));
|
||||
return state.copyWith(
|
||||
valid: false, validationState: state.validationState);
|
||||
}
|
||||
|
||||
if (validatePosition(shift.bloc.state.roles)) {
|
||||
return state.copyWith(
|
||||
valid: false, validationState: state.validationState);
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
static bool validatePosition(List<RoleViewModel> roles) {
|
||||
for (var position in roles) {
|
||||
PositionValidationState? validationState;
|
||||
|
||||
if (position.bloc.state.entity.businessSkill == null) {
|
||||
validationState =
|
||||
PositionValidationState(skillError: 'Skill cannot be empty');
|
||||
}
|
||||
|
||||
if (position.bloc.state.entity.department == null) {
|
||||
validationState = PositionValidationState(
|
||||
departmentError: 'Department cannot be empty');
|
||||
}
|
||||
|
||||
if (position.bloc.state.entity.count == null) {
|
||||
validationState =
|
||||
PositionValidationState(countError: 'Count cannot be empty');
|
||||
}
|
||||
|
||||
if (validationState != null) {
|
||||
position.bloc.add(ValidationPositionFailedEvent(validationState));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:collection/collection.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/data/models/event/event_model.dart';
|
||||
import 'package:krow/core/entity/event_entity.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_app_bar.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_popup_button.dart';
|
||||
import 'package:krow/features/create_event/domain/bloc/create_event_bloc.dart';
|
||||
import 'package:krow/features/create_event/presentation/create_shift_details_section/create_shifts_list.dart';
|
||||
import 'package:krow/features/create_event/presentation/event_date_section/bloc/date_selector_bloc.dart';
|
||||
import 'package:krow/features/create_event/presentation/widgets/add_info_input_widget.dart';
|
||||
import 'package:krow/features/create_event/presentation/widgets/addons_section_widget.dart';
|
||||
import 'package:krow/features/create_event/presentation/widgets/create_event_details_card_widget.dart';
|
||||
import 'package:krow/features/create_event/presentation/widgets/create_event_tags_card.dart';
|
||||
import 'package:krow/features/create_event/presentation/widgets/create_event_title_widget.dart';
|
||||
import 'package:krow/features/create_event/presentation/widgets/total_cost_row_widget.dart';
|
||||
import 'package:modal_progress_hud_nsn/modal_progress_hud_nsn.dart';
|
||||
|
||||
@RoutePage()
|
||||
class CreateEventScreen extends StatelessWidget implements AutoRouteWrapper {
|
||||
final EventScheduleType? eventType;
|
||||
final EventModel? eventModel;
|
||||
|
||||
const CreateEventScreen({super.key, this.eventType, this.eventModel});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<CreateEventBloc, CreateEventState>(
|
||||
listener: (context, state) {
|
||||
if (state.success) {
|
||||
context.router.maybePop();
|
||||
}
|
||||
|
||||
if (state.valid) {
|
||||
context.router
|
||||
.push(EventDetailsRoute(event: state.entity, isPreview: true));
|
||||
}
|
||||
_checkValidation(state, context);
|
||||
},
|
||||
builder: (context, state) {
|
||||
return ModalProgressHUD(
|
||||
inAsyncCall: state.inLoading,
|
||||
child: Scaffold(
|
||||
appBar: KwAppBar(
|
||||
titleText: 'Create Event',
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: CustomScrollView(
|
||||
primary: false,
|
||||
slivers: [
|
||||
SliverList.list(
|
||||
children: const [
|
||||
CreateEventTitleWidget(),
|
||||
CreateEventDetailsCardWidget(),
|
||||
CreateEventTagsCard(),
|
||||
],
|
||||
),
|
||||
const CreateShiftsList(),
|
||||
const SliverGap(24),
|
||||
sliverWrap(
|
||||
KwButton.outlinedPrimary(
|
||||
onPressed: () {
|
||||
BlocProvider.of<CreateEventBloc>(context)
|
||||
.add(CreateEventAddShift());
|
||||
},
|
||||
label: 'Add Shift',
|
||||
leftIcon: Assets.images.icons.add,
|
||||
),
|
||||
),
|
||||
const SliverGap(24),
|
||||
sliverWrap(const AddonsSectionWidget()),
|
||||
const SliverGap(24),
|
||||
sliverWrap(const AddInfoInputWidget()),
|
||||
const SliverGap(24),
|
||||
sliverWrap( TotalCostRowWidget()),
|
||||
const SliverGap(24),
|
||||
SliverSafeArea(
|
||||
top: false,
|
||||
sliver: SliverPadding(
|
||||
padding: const EdgeInsets.only(bottom: 36, top: 36),
|
||||
sliver: sliverWrap(
|
||||
KwPopUpButton(
|
||||
disabled: state.validationState != null,
|
||||
label: state.entity.id.isEmpty
|
||||
? 'Save as Draft'
|
||||
: state.entity.status == EventStatus.draft
|
||||
? 'Update Draft'
|
||||
: 'Update Event',
|
||||
popUpPadding: 16,
|
||||
items: [
|
||||
KwPopUpButtonItem(
|
||||
title: 'Preview',
|
||||
onTap: () {
|
||||
BlocProvider.of<CreateEventBloc>(context)
|
||||
.add(CreateEventValidateAndPreview());
|
||||
}),
|
||||
if (state.entity.status == EventStatus.draft)
|
||||
KwPopUpButtonItem(
|
||||
title: 'Delete Event Draft',
|
||||
onTap: () {
|
||||
BlocProvider.of<CreateEventBloc>(context)
|
||||
.add(DeleteDraftEvent());
|
||||
context.router.popUntilRoot();
|
||||
context.router.maybePop();
|
||||
},
|
||||
color: AppColors.statusError),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
sliverWrap(Widget widget) {
|
||||
return SliverToBoxAdapter(
|
||||
child: widget,
|
||||
);
|
||||
}
|
||||
|
||||
void _checkValidation(CreateEventState state, BuildContext context) {
|
||||
if (state.validationState != null && !state.validationState!.showed) {
|
||||
state.validationState?.showed = true;
|
||||
for (var e in state.shifts) {
|
||||
e.bloc.state.validationState?.showed = true;
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(state.validationState?.message ?? ''),
|
||||
backgroundColor: AppColors.statusError,
|
||||
));
|
||||
} else {
|
||||
var invalidShift = state.shifts
|
||||
.firstWhereOrNull((e) => e.bloc.state.validationState != null);
|
||||
|
||||
if (invalidShift != null &&
|
||||
!invalidShift.bloc.state.validationState!.showed) {
|
||||
invalidShift.bloc.state.validationState?.showed = true;
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(invalidShift.bloc.state.validationState?.message ?? ''),
|
||||
backgroundColor: AppColors.statusError,
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
var invalidRole = state.shifts
|
||||
.expand((shift) => shift.bloc.state.roles)
|
||||
.firstWhereOrNull((role) => role.bloc.state.validationState != null);
|
||||
|
||||
if(invalidRole != null && !invalidRole.bloc.state.validationState!.showed) {
|
||||
invalidRole.bloc.state.validationState?.showed = true;
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(invalidRole.bloc.state.validationState?.message ?? ''),
|
||||
backgroundColor: AppColors.statusError,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget wrappedRoute(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (_) =>
|
||||
CreateEventBloc()..add(CreateEventInit(eventType, eventModel))),
|
||||
BlocProvider(
|
||||
create: (_) => DateSelectorBloc()
|
||||
..add(DateSelectorEventInit(eventType, eventModel))),
|
||||
],
|
||||
child: this,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/core/data/models/event/skill.dart';
|
||||
import 'package:krow/core/data/models/shift/business_skill_model.dart';
|
||||
import 'package:krow/core/data/models/shift/department_model.dart';
|
||||
import 'package:krow/core/entity/position_entity.dart';
|
||||
import 'package:krow/core/entity/role_schedule_entity.dart';
|
||||
|
||||
part 'create_role_event.dart';
|
||||
part 'create_role_state.dart';
|
||||
|
||||
class CreateRoleBloc extends Bloc<CreateRoleEvent, CreateRoleState> {
|
||||
CreateRoleBloc()
|
||||
: super(CreateRoleState(
|
||||
isExpanded: true,
|
||||
entity: PositionEntity.empty(),
|
||||
)) {
|
||||
on<CreateRoleInitEvent>(_onRoleInit);
|
||||
on<CreateRoleSelectSkillEvent>(_onRoleSelect);
|
||||
on<ExpandRoleEvent>(_onExpandRole);
|
||||
on<SetRoleStartTimeEvent>(_onSetRoleStartTime);
|
||||
on<SetRoleEndTimeEvent>(_onSetRoleEndTime);
|
||||
on<CreateRoleSelectDepartmentEvent>(_onSelectDepartment);
|
||||
on<CreateRoleSelectCountEvent>(_onSelectCount);
|
||||
on<CreateRoleSetScheduleEvent>(_onSetRoleSchedule);
|
||||
on<CreateRoleSelectBreak>(_onSetRoleBreak);
|
||||
on<ValidationPositionFailedEvent>(_onValidationFailed);
|
||||
}
|
||||
|
||||
FutureOr<void> _onRoleInit(CreateRoleInitEvent event, emit) {
|
||||
emit(state.copyWith(entity: event.role));
|
||||
}
|
||||
|
||||
FutureOr<void> _onRoleSelect(CreateRoleSelectSkillEvent event, emit) {
|
||||
emit(state.copyWith(entity: state.entity.copyWith(businessSkill: event.skill)));
|
||||
}
|
||||
|
||||
FutureOr<void> _onExpandRole(event, emit) {
|
||||
emit(state.copyWith(isExpanded: !state.isExpanded));
|
||||
}
|
||||
|
||||
FutureOr<void> _onSetRoleStartTime(event, emit) {
|
||||
emit(state.copyWith(
|
||||
entity: state.entity.copyWith(startTime: event.startTime)));
|
||||
}
|
||||
|
||||
FutureOr<void> _onSetRoleEndTime(event, emit) {
|
||||
emit(state.copyWith(entity: state.entity.copyWith(endTime: event.endTime)));
|
||||
}
|
||||
|
||||
FutureOr<void> _onSelectDepartment(
|
||||
CreateRoleSelectDepartmentEvent event, emit) {
|
||||
emit(state.copyWith(entity: state.entity.copyWith(department: event.item)));
|
||||
}
|
||||
|
||||
FutureOr<void> _onSelectCount(CreateRoleSelectCountEvent event, emit) {
|
||||
emit(state.copyWith(entity: state.entity.copyWith(count: event.item)));
|
||||
}
|
||||
|
||||
FutureOr<void> _onSetRoleSchedule(CreateRoleSetScheduleEvent event, emit) {
|
||||
emit(state.copyWith(
|
||||
entity: state.entity.copyWith(schedule: event.schedule)));
|
||||
}
|
||||
|
||||
FutureOr<void> _onSetRoleBreak(CreateRoleSelectBreak event, emit) {
|
||||
emit(state.copyWith(entity: state.entity.copyWith(breakDuration: event.item)));
|
||||
}
|
||||
|
||||
FutureOr<void> _onValidationFailed(ValidationPositionFailedEvent event, emit) {
|
||||
emit(state.copyWith(validationState: event.validationState));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
part of 'create_role_bloc.dart';
|
||||
|
||||
@immutable
|
||||
sealed class CreateRoleEvent {}
|
||||
|
||||
class CreateRoleInitEvent extends CreateRoleEvent {
|
||||
final PositionEntity role;
|
||||
|
||||
CreateRoleInitEvent(this.role);
|
||||
}
|
||||
|
||||
class CreateRoleSelectSkillEvent extends CreateRoleEvent {
|
||||
final BusinessSkillModel skill;
|
||||
|
||||
CreateRoleSelectSkillEvent(this.skill);
|
||||
}
|
||||
|
||||
class ExpandRoleEvent extends CreateRoleEvent {
|
||||
ExpandRoleEvent();
|
||||
}
|
||||
|
||||
class SetRoleStartTimeEvent extends CreateRoleEvent {
|
||||
final DateTime startTime;
|
||||
|
||||
SetRoleStartTimeEvent(this.startTime);
|
||||
}
|
||||
|
||||
class SetRoleEndTimeEvent extends CreateRoleEvent {
|
||||
final DateTime endTime;
|
||||
|
||||
SetRoleEndTimeEvent(this.endTime);
|
||||
}
|
||||
|
||||
class CreateRoleSelectDepartmentEvent extends CreateRoleEvent {
|
||||
final DepartmentModel item;
|
||||
|
||||
CreateRoleSelectDepartmentEvent(this.item);
|
||||
}
|
||||
|
||||
class CreateRoleSelectCountEvent extends CreateRoleEvent {
|
||||
final int item;
|
||||
|
||||
CreateRoleSelectCountEvent(this.item);
|
||||
}
|
||||
|
||||
class CreateRoleSelectBreak extends CreateRoleEvent {
|
||||
final int item;
|
||||
|
||||
CreateRoleSelectBreak(this.item);
|
||||
}
|
||||
|
||||
class CreateRoleSetScheduleEvent extends CreateRoleEvent {
|
||||
final List<RoleScheduleEntity> schedule;
|
||||
|
||||
CreateRoleSetScheduleEvent(this.schedule);
|
||||
}
|
||||
|
||||
class ValidationPositionFailedEvent extends CreateRoleEvent {
|
||||
final PositionValidationState validationState;
|
||||
|
||||
ValidationPositionFailedEvent(this.validationState);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
part of 'create_role_bloc.dart';
|
||||
|
||||
@immutable
|
||||
class CreateRoleState {
|
||||
final bool isExpanded;
|
||||
final PositionEntity entity;
|
||||
final PositionValidationState? validationState;
|
||||
|
||||
const CreateRoleState({
|
||||
this.isExpanded = true,
|
||||
required this.entity,
|
||||
this.validationState,
|
||||
});
|
||||
|
||||
CreateRoleState copyWith(
|
||||
{bool? isExpanded,
|
||||
PositionEntity? entity,
|
||||
PositionValidationState? validationState}) {
|
||||
return CreateRoleState(
|
||||
isExpanded: isExpanded ?? this.isExpanded,
|
||||
entity: entity ?? this.entity,
|
||||
validationState: validationState,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PositionValidationState {
|
||||
final String? skillError;
|
||||
final String? departmentError;
|
||||
final String? countError;
|
||||
bool showed;
|
||||
|
||||
bool get hasError =>
|
||||
skillError != null || countError != null || departmentError != null;
|
||||
|
||||
String? get message {
|
||||
return skillError ?? countError ?? departmentError ?? '';
|
||||
}
|
||||
|
||||
PositionValidationState(
|
||||
{this.skillError,
|
||||
this.countError,
|
||||
this.departmentError,
|
||||
this.showed = false});
|
||||
|
||||
PositionValidationState copyWith({
|
||||
String? skillError,
|
||||
String? countError,
|
||||
String? departmentError,
|
||||
}) {
|
||||
return PositionValidationState(
|
||||
skillError: skillError ?? this.skillError,
|
||||
countError: countError ?? this.countError,
|
||||
departmentError: departmentError ?? this.departmentError,
|
||||
showed: showed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
import 'package:expandable/expandable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/data/models/event/event_model.dart';
|
||||
import 'package:krow/core/data/models/event/skill.dart';
|
||||
import 'package:krow/core/data/models/shift/business_skill_model.dart';
|
||||
import 'package:krow/core/data/models/shift/department_model.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/kw_time_slot.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_dropdown.dart';
|
||||
import 'package:krow/features/create_event/domain/bloc/create_event_bloc.dart';
|
||||
import 'package:krow/features/create_event/presentation/create_role_section/bloc/create_role_bloc.dart';
|
||||
import 'package:krow/features/create_event/presentation/create_shift_details_section/bloc/create_shift_details_bloc.dart';
|
||||
import 'package:krow/features/create_event/presentation/role_schedule_dialog/recurring_schedule_widget.dart';
|
||||
|
||||
class CreateRoleDetailsWidget extends StatefulWidget {
|
||||
final String id;
|
||||
|
||||
const CreateRoleDetailsWidget({
|
||||
super.key,
|
||||
required this.id,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CreateRoleDetailsWidget> createState() =>
|
||||
_CreateRoleDetailsWidgetState();
|
||||
}
|
||||
|
||||
class _CreateRoleDetailsWidgetState extends State<CreateRoleDetailsWidget> {
|
||||
ExpandableController? _expandableController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_expandableController = ExpandableController(initialExpanded: true);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_expandableController?.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CreateEventBloc, CreateEventState>(
|
||||
builder: (context, eventState) {
|
||||
return BlocBuilder<CreateShiftDetailsBloc, CreateShiftDetailsState>(
|
||||
builder: (context, shiftState) {
|
||||
return BlocConsumer<CreateRoleBloc, CreateRoleState>(
|
||||
listener: (context, state) {
|
||||
if (state.isExpanded != _expandableController?.expanded) {
|
||||
_expandableController!.toggle();
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.only(top: 12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: KwBoxDecorations.primaryLight8.copyWith(
|
||||
border: state.validationState != null
|
||||
? Border.all(color: AppColors.statusError, width: 1)
|
||||
: null),
|
||||
child: ExpandableTheme(
|
||||
data: const ExpandableThemeData(
|
||||
hasIcon: false,
|
||||
animationDuration: Duration(milliseconds: 250)),
|
||||
child: ExpandablePanel(
|
||||
collapsed: Container(),
|
||||
controller: _expandableController,
|
||||
header: _buildHeader(state, shiftState, context),
|
||||
expanded: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_skillDropDown(state.entity.businessSkill,
|
||||
eventState.skills, context),
|
||||
const Gap(8),
|
||||
_departmentDropDown(state.entity.department,
|
||||
eventState.departments, context),
|
||||
const Gap(8),
|
||||
_timeRow(context, state.entity.startTime,
|
||||
state.entity.endTime),
|
||||
const Gap(8),
|
||||
_countDropDown(state.entity.count, context),
|
||||
const Gap(8),
|
||||
_breakDropDown(context, state),
|
||||
if (eventState.recurringType ==
|
||||
EventScheduleType.recurring)
|
||||
const RecurringScheduleWidget(),
|
||||
const Gap(12),
|
||||
textRow('Cost',
|
||||
'\$${state.entity.businessSkill?.price?.toStringAsFixed(2) ?? '0'}'),
|
||||
const Gap(12),
|
||||
textRow('Value', '\$${state.entity.price.toStringAsFixed(2)}'),
|
||||
const Gap(12),
|
||||
KwButton.accent(
|
||||
label: 'Save Role',
|
||||
onPressed: () {
|
||||
context
|
||||
.read<CreateRoleBloc>()
|
||||
.add(ExpandRoleEvent());
|
||||
}),
|
||||
const Gap(12),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(CreateRoleState state, CreateShiftDetailsState shiftState,
|
||||
BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
context.read<CreateRoleBloc>().add(ExpandRoleEvent());
|
||||
},
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
padding: const EdgeInsets.only(bottom: 12, top: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
state.entity.businessSkill?.skill?.name ?? 'New Role',
|
||||
style: AppTextStyles.bodyLargeMed,
|
||||
),
|
||||
if (!state.isExpanded)
|
||||
Center(
|
||||
child: Assets.images.icons.chevronDown.svg(
|
||||
height: 24,
|
||||
width: 24,
|
||||
colorFilter: const ColorFilter.mode(
|
||||
AppColors.grayStroke,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (state.isExpanded && shiftState.roles.length > 1)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context
|
||||
.read<CreateShiftDetailsBloc>()
|
||||
.add(DeleteRoleDeleteEvent(widget.id));
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 4),
|
||||
child: Assets.images.icons.delete.svg(
|
||||
width: 16,
|
||||
height: 16,
|
||||
colorFilter: const ColorFilter.mode(
|
||||
AppColors.statusError, BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_skillDropDown(BusinessSkillModel? selectedSkill,
|
||||
List<BusinessSkillModel> allSkills, BuildContext context) {
|
||||
return KwDropdown(
|
||||
key: ValueKey('skill_key${widget.id}'),
|
||||
title: 'Role',
|
||||
hintText: 'Role name',
|
||||
horizontalPadding: 40,
|
||||
selectedItem: selectedSkill != null
|
||||
? KwDropDownItem(
|
||||
data: selectedSkill, title: selectedSkill.skill?.name ?? '')
|
||||
: null,
|
||||
items: allSkills
|
||||
.map((e) => KwDropDownItem(data: e, title: e.skill?.name ?? '')),
|
||||
onSelected: (item) {
|
||||
context.read<CreateRoleBloc>().add(CreateRoleSelectSkillEvent(item));
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 500),
|
||||
() => context
|
||||
.read<CreateEventBloc>()
|
||||
.add(CreateEventEntityUpdatedEvent()));
|
||||
});
|
||||
}
|
||||
|
||||
_departmentDropDown(DepartmentModel? selected,
|
||||
List<DepartmentModel> allDepartments, BuildContext context) {
|
||||
return KwDropdown(
|
||||
title: 'Department',
|
||||
hintText: 'Department name',
|
||||
horizontalPadding: 40,
|
||||
selectedItem: selected != null
|
||||
? KwDropDownItem(data: selected, title: selected.name)
|
||||
: null,
|
||||
items:
|
||||
allDepartments.map((e) => KwDropDownItem(data: e, title: e.name)),
|
||||
onSelected: (item) {
|
||||
context
|
||||
.read<CreateRoleBloc>()
|
||||
.add(CreateRoleSelectDepartmentEvent(item));
|
||||
});
|
||||
}
|
||||
|
||||
_countDropDown(int? selected, BuildContext context) {
|
||||
return KwDropdown(
|
||||
key: ValueKey('count_key${widget.id}'),
|
||||
title: 'Number of Employee for one Role',
|
||||
hintText: 'Person count',
|
||||
horizontalPadding: 40,
|
||||
selectedItem: selected != null
|
||||
? KwDropDownItem(data: selected, title: selected.toString())
|
||||
: null,
|
||||
items: List.generate(98, (e) => e + 1)
|
||||
.map((e) => KwDropDownItem(data: e, title: (e).toString())),
|
||||
onSelected: (item) {
|
||||
context.read<CreateRoleBloc>().add(CreateRoleSelectCountEvent(item));
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 500),
|
||||
() {
|
||||
context
|
||||
.read<CreateEventBloc>()
|
||||
.add(CreateEventEntityUpdatedEvent());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_timeRow(context, DateTime? startTime, DateTime? endTime) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: KwTimeSlotInput(
|
||||
key: ValueKey('start_time_key${widget.id}'),
|
||||
label: 'Start Time',
|
||||
initialValue: startTime ?? DateTime(2000,1,1,9,0),
|
||||
onChange: (value) {
|
||||
|
||||
BlocProvider.of<CreateRoleBloc>(context)
|
||||
.add(SetRoleStartTimeEvent(value));
|
||||
try {
|
||||
// Future.delayed(
|
||||
// const Duration(milliseconds: 100),
|
||||
// () => context
|
||||
// .read<CreateEventBloc>()
|
||||
// .add(CreateEventEntityUpdatedEvent()));
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: KwTimeSlotInput(
|
||||
key: ValueKey('end_time_key${widget.id}'),
|
||||
label: 'End Time',
|
||||
initialValue: endTime ?? DateTime(2000,1,1,14,0),
|
||||
onChange: (value) {
|
||||
BlocProvider.of<CreateRoleBloc>(context)
|
||||
.add(SetRoleEndTimeEvent(value));
|
||||
try {
|
||||
// Future.delayed(
|
||||
// const Duration(milliseconds: 100),
|
||||
// () => context
|
||||
// .read<CreateEventBloc>()
|
||||
// .add(CreateEventEntityUpdatedEvent()));
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
_breakDropDown(BuildContext context,CreateRoleState state) {
|
||||
String formatBreakDuration(int minutes) {
|
||||
if (minutes == 60) {
|
||||
return '1h';
|
||||
} else if (minutes > 60) {
|
||||
final hours = minutes ~/ 60;
|
||||
final remainder = minutes % 60;
|
||||
return remainder == 0 ? '${hours}h' : '${hours}h ${remainder} min';
|
||||
}
|
||||
return '${minutes} min';
|
||||
}
|
||||
|
||||
|
||||
|
||||
return KwDropdown(
|
||||
key: ValueKey('break_key${widget.id}'),
|
||||
title: 'Break Duration',
|
||||
hintText: 'Duration',
|
||||
horizontalPadding: 40,
|
||||
selectedItem: KwDropDownItem(data: state.entity.breakDuration, title: formatBreakDuration(state.entity.breakDuration??0)),
|
||||
items: [15, 30].map((e) {
|
||||
return KwDropDownItem(data: e, title: formatBreakDuration(e));
|
||||
}),
|
||||
onSelected: (item) {
|
||||
context.read<CreateRoleBloc>().add(CreateRoleSelectBreak(item??0));
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
Widget textRow(String key, String value) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(key,
|
||||
style: AppTextStyles.bodyMediumReg
|
||||
.copyWith(color: AppColors.blackGray)),
|
||||
Text(value, style: AppTextStyles.bodyMediumMed),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/features/create_event/presentation/create_role_section/create_role_widget.dart';
|
||||
import 'package:krow/features/create_event/presentation/create_shift_details_section/bloc/create_shift_details_bloc.dart';
|
||||
|
||||
class CreateRolesList extends StatefulWidget {
|
||||
final String id;
|
||||
|
||||
const CreateRolesList({super.key, required this.id});
|
||||
|
||||
@override
|
||||
State<CreateRolesList> createState() => _CreateRolesListState();
|
||||
}
|
||||
|
||||
class _CreateRolesListState extends State<CreateRolesList> {
|
||||
final Map<String, GlobalKey> roleKeys = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CreateShiftDetailsBloc, CreateShiftDetailsState>(
|
||||
builder: (context, state) {
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(0),
|
||||
shrinkWrap: true,
|
||||
primary: false,
|
||||
itemCount: state.roles.length,
|
||||
itemBuilder: (context, index) {
|
||||
final roleId = state.roles[index].id;
|
||||
if (roleKeys[roleId] == null) {
|
||||
roleKeys[roleId] = GlobalKey();
|
||||
}
|
||||
return BlocProvider.value(
|
||||
value: state.roles[index].bloc,
|
||||
child: CreateRoleDetailsWidget(
|
||||
key: roleKeys[roleId], id: state.roles[index].id),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:krow/core/data/models/event/business_member_model.dart';
|
||||
import 'package:krow/core/data/models/event/full_address_model.dart';
|
||||
import 'package:krow/core/entity/position_entity.dart';
|
||||
import 'package:krow/core/entity/shift_entity.dart';
|
||||
import 'package:krow/features/create_event/domain/google_places_service.dart';
|
||||
import 'package:krow/features/create_event/presentation/create_role_section/bloc/create_role_bloc.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
part 'create_shift_details_event.dart';
|
||||
part 'create_shift_details_state.dart';
|
||||
|
||||
class CreateShiftDetailsBloc
|
||||
extends Bloc<CreateShiftDetailsEvent, CreateShiftDetailsState> {
|
||||
CreateShiftDetailsBloc({required bool expanded})
|
||||
: super(CreateShiftDetailsState(
|
||||
suggestions: [],
|
||||
isExpanded: expanded,
|
||||
shift: ShiftEntity.empty(),
|
||||
roles: [
|
||||
RoleViewModel(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
bloc: CreateRoleBloc()),
|
||||
])) {
|
||||
on<CreateShiftInitializeEvent>(_onInitialize);
|
||||
on<CreateShiftAddressQueryChangedEvent>(_onQueryChanged);
|
||||
on<CreateShiftAddressSelectEvent>(_onAddressSelect);
|
||||
on<CreateShiftSelectContactEvent>(_onSelectContact);
|
||||
on<CreateShiftRemoveContactEvent>(_onRemoveContact);
|
||||
on<CreateEventAddRoleEvent>(_onAddRole);
|
||||
on<DeleteRoleDeleteEvent>(_onDeleteRole);
|
||||
on<ExpandShiftEvent>(_onExpandShift);
|
||||
on<ValidationFailedEvent>((event, emit) {
|
||||
emit(state.copyWith(validationState: event.validationState));
|
||||
});
|
||||
}
|
||||
|
||||
void _onInitialize(CreateShiftInitializeEvent event, emit) async {
|
||||
emit(CreateShiftDetailsState(
|
||||
suggestions: [],
|
||||
isExpanded: state.isExpanded,
|
||||
shift: event.shift,
|
||||
roles: [
|
||||
...event.shift.positions.map((roleEntity) => RoleViewModel(
|
||||
id: roleEntity.id,
|
||||
bloc: CreateRoleBloc()
|
||||
..add(
|
||||
CreateRoleInitEvent(roleEntity),
|
||||
))),
|
||||
]));
|
||||
}
|
||||
|
||||
void _onQueryChanged(CreateShiftAddressQueryChangedEvent event, emit) async {
|
||||
try {
|
||||
final googlePlacesService = GooglePlacesService();
|
||||
final suggestions =
|
||||
await googlePlacesService.fetchSuggestions(event.query);
|
||||
emit(state.copyWith(suggestions: suggestions));
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
|
||||
void _onAddressSelect(CreateShiftAddressSelectEvent event, emit) async {
|
||||
if (event.address != null) {
|
||||
emit(state.copyWith(
|
||||
suggestions: [],
|
||||
shift: state.shift.copyWith(fullAddress: event.address!)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.place != null) {
|
||||
final googlePlacesService = GooglePlacesService();
|
||||
final fullAddress =
|
||||
await googlePlacesService.getPlaceDetails(event.place!.placeId);
|
||||
FullAddress address = FullAddress.fromGoogle(fullAddress);
|
||||
emit(state.copyWith(
|
||||
suggestions: [], shift: state.shift.copyWith(fullAddress: address)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void _onSelectContact(CreateShiftSelectContactEvent event, emit) {
|
||||
emit(state.copyWith(
|
||||
shift: state.shift
|
||||
.copyWith(managers: [...state.shift.managers, event.contact])));
|
||||
}
|
||||
|
||||
void _onRemoveContact(CreateShiftRemoveContactEvent event, emit) {
|
||||
emit(state.copyWith(
|
||||
shift: state.shift.copyWith(
|
||||
managers: state.shift.managers
|
||||
.where((element) => element != event.contact)
|
||||
.toList())));
|
||||
}
|
||||
|
||||
void _onAddRole(CreateEventAddRoleEvent event, emit) {
|
||||
final id = DateTime.now().millisecondsSinceEpoch.toString();
|
||||
final bloc = CreateRoleBloc();
|
||||
|
||||
PositionEntity newPosition = PositionEntity.empty();
|
||||
|
||||
bloc.add(CreateRoleInitEvent(newPosition));
|
||||
|
||||
newPosition.parentShift = state.shift;
|
||||
state.shift.positions.add(newPosition);
|
||||
|
||||
emit(state.copyWith(
|
||||
roles: [
|
||||
...state.roles,
|
||||
RoleViewModel(id: id, bloc: bloc),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
void _onDeleteRole(DeleteRoleDeleteEvent event, emit) {
|
||||
emit(state.copyWith(
|
||||
roles: state.roles.where((element) => element.id != event.id).toList(),
|
||||
));
|
||||
}
|
||||
|
||||
void _onExpandShift(ExpandShiftEvent event, emit) {
|
||||
emit(state.copyWith(isExpanded: !state.isExpanded));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
part of 'create_shift_details_bloc.dart';
|
||||
|
||||
@immutable
|
||||
sealed class CreateShiftDetailsEvent {}
|
||||
|
||||
class CreateShiftInitializeEvent extends CreateShiftDetailsEvent {
|
||||
final ShiftEntity shift;
|
||||
|
||||
CreateShiftInitializeEvent(this.shift);
|
||||
}
|
||||
|
||||
class CreateShiftAddressSelectEvent extends CreateShiftDetailsEvent {
|
||||
final MapPlace? place;
|
||||
final FullAddress? address;
|
||||
|
||||
CreateShiftAddressSelectEvent({this.place, this.address});
|
||||
}
|
||||
|
||||
class CreateShiftAddressQueryChangedEvent extends CreateShiftDetailsEvent {
|
||||
final String query;
|
||||
|
||||
CreateShiftAddressQueryChangedEvent(this.query);
|
||||
}
|
||||
|
||||
class CreateShiftSelectContactEvent extends CreateShiftDetailsEvent {
|
||||
final BusinessMemberModel contact;
|
||||
|
||||
CreateShiftSelectContactEvent(this.contact);
|
||||
}
|
||||
|
||||
class CreateShiftRemoveContactEvent extends CreateShiftDetailsEvent {
|
||||
final BusinessMemberModel contact;
|
||||
|
||||
CreateShiftRemoveContactEvent(this.contact);
|
||||
}
|
||||
|
||||
class CreateEventAddRoleEvent extends CreateShiftDetailsEvent {
|
||||
CreateEventAddRoleEvent();
|
||||
}
|
||||
|
||||
class DeleteRoleDeleteEvent extends CreateShiftDetailsEvent {
|
||||
final String id;
|
||||
|
||||
DeleteRoleDeleteEvent(this.id);
|
||||
}
|
||||
|
||||
class ExpandShiftEvent extends CreateShiftDetailsEvent {
|
||||
ExpandShiftEvent();
|
||||
}
|
||||
|
||||
class ValidationFailedEvent extends CreateShiftDetailsEvent {
|
||||
final ShiftValidationState validationState;
|
||||
|
||||
ValidationFailedEvent(this.validationState);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
part of 'create_shift_details_bloc.dart';
|
||||
|
||||
@immutable
|
||||
class CreateShiftDetailsState {
|
||||
final bool inLoading;
|
||||
final bool isExpanded;
|
||||
final ShiftEntity shift;
|
||||
final List<MapPlace> suggestions;
|
||||
final List<RoleViewModel> roles;
|
||||
|
||||
final ShiftValidationState? validationState;
|
||||
|
||||
const CreateShiftDetailsState({
|
||||
required this.shift,
|
||||
this.inLoading = false,
|
||||
required this.suggestions,
|
||||
required this.roles,
|
||||
this.isExpanded = true,
|
||||
this.validationState
|
||||
});
|
||||
|
||||
CreateShiftDetailsState copyWith({
|
||||
bool? inLoading,
|
||||
List<MapPlace>? suggestions,
|
||||
ShiftEntity? shift,
|
||||
String? department,
|
||||
List<RoleViewModel>? roles,
|
||||
bool? isExpanded,
|
||||
ShiftValidationState? validationState,
|
||||
}) {
|
||||
return CreateShiftDetailsState(
|
||||
shift: shift ?? this.shift,
|
||||
inLoading: inLoading ?? false,
|
||||
suggestions: suggestions ?? this.suggestions,
|
||||
roles: roles ?? this.roles,
|
||||
isExpanded: isExpanded ?? this.isExpanded,
|
||||
validationState: validationState,
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RoleViewModel {
|
||||
final String id;
|
||||
final CreateRoleBloc bloc;
|
||||
|
||||
|
||||
RoleViewModel({required this.id, required this.bloc});
|
||||
}
|
||||
|
||||
class ShiftValidationState {
|
||||
|
||||
final String? addressError;
|
||||
final String? contactsError;
|
||||
bool showed;
|
||||
|
||||
bool get hasError =>
|
||||
addressError != null ||
|
||||
contactsError != null;
|
||||
|
||||
String? get message {
|
||||
return addressError ?? contactsError ?? '';
|
||||
}
|
||||
|
||||
ShiftValidationState({
|
||||
this.addressError,
|
||||
this.contactsError,this.showed = false});
|
||||
|
||||
ShiftValidationState copyWith({
|
||||
String? addressError,
|
||||
String? contactsError,
|
||||
}) {
|
||||
return ShiftValidationState(
|
||||
addressError: addressError ?? this.addressError,
|
||||
contactsError: contactsError ?? this.contactsError,
|
||||
showed: showed
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import 'package:expandable/expandable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_suggestion_input.dart';
|
||||
import 'package:krow/features/create_event/domain/bloc/create_event_bloc.dart';
|
||||
import 'package:krow/features/create_event/domain/google_places_service.dart';
|
||||
import 'package:krow/features/create_event/presentation/create_role_section/create_roles_list.dart';
|
||||
import 'package:krow/features/create_event/presentation/create_shift_details_section/bloc/create_shift_details_bloc.dart';
|
||||
import 'package:krow/features/create_event/presentation/create_shift_details_section/widgets/shift_contacts_widget.dart';
|
||||
|
||||
class CreateShiftDetailsWidget extends StatefulWidget {
|
||||
final String id;
|
||||
|
||||
final int index;
|
||||
|
||||
final bool expanded;
|
||||
|
||||
const CreateShiftDetailsWidget({
|
||||
super.key,
|
||||
required this.id,
|
||||
required this.index,
|
||||
this.expanded = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CreateShiftDetailsWidget> createState() =>
|
||||
_CreateShiftDetailsWidgetState();
|
||||
}
|
||||
|
||||
class _CreateShiftDetailsWidgetState extends State<CreateShiftDetailsWidget>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
ExpandableController? _expandableController;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_expandableController =
|
||||
ExpandableController(initialExpanded: widget.expanded);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_expandableController?.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return BlocBuilder<CreateEventBloc, CreateEventState>(
|
||||
builder: (context, eventState) {
|
||||
return BlocConsumer<CreateShiftDetailsBloc, CreateShiftDetailsState>(
|
||||
listener: (context, state) {
|
||||
if (state.isExpanded != _expandableController?.expanded) {
|
||||
_expandableController!.toggle();
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.only(top: 12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: KwBoxDecorations.white12.copyWith(
|
||||
border: state.validationState != null
|
||||
? Border.all(color: AppColors.statusError, width: 1)
|
||||
: null),
|
||||
child: ExpandableTheme(
|
||||
data: const ExpandableThemeData(
|
||||
hasIcon: false,
|
||||
animationDuration: Duration(milliseconds: 250)),
|
||||
child: ExpandablePanel(
|
||||
collapsed: Container(),
|
||||
controller: _expandableController,
|
||||
header: _buildHeader(
|
||||
context, state, eventState.shifts.length > 1),
|
||||
expanded: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_addressInput(state, context),
|
||||
ShiftContactsWidget(
|
||||
allContacts: eventState.contacts,
|
||||
selectedContacts: state.shift?.managers ?? []),
|
||||
CreateRolesList(id: widget.id),
|
||||
const Gap(12),
|
||||
KwButton.outlinedPrimary(
|
||||
label: 'Add New Role',
|
||||
leftIcon: Assets.images.icons.add,
|
||||
onPressed: () {
|
||||
context
|
||||
.read<CreateShiftDetailsBloc>()
|
||||
.add(CreateEventAddRoleEvent());
|
||||
}),
|
||||
const Gap(12),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(
|
||||
BuildContext context,
|
||||
CreateShiftDetailsState state,
|
||||
bool canBeRemoved,
|
||||
) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
context.read<CreateShiftDetailsBloc>().add(ExpandShiftEvent());
|
||||
},
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Shift Details #${widget.index + 1}',
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
),
|
||||
if (!state.isExpanded)
|
||||
Center(
|
||||
child: Assets.images.icons.chevronDown.svg(
|
||||
height: 24,
|
||||
width: 24,
|
||||
colorFilter: const ColorFilter.mode(
|
||||
AppColors.grayStroke,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (state.isExpanded && canBeRemoved)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context
|
||||
.read<CreateEventBloc>()
|
||||
.add(CreateEventRemoveShift(widget.id));
|
||||
},
|
||||
child: Container(
|
||||
height: 24,
|
||||
width: 24,
|
||||
color: Colors.transparent,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 4),
|
||||
child: Center(
|
||||
child: Assets.images.icons.delete.svg(
|
||||
width: 16,
|
||||
height: 16,
|
||||
colorFilter: const ColorFilter.mode(
|
||||
AppColors.statusError, BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
KwSuggestionInput<MapPlace> _addressInput(
|
||||
CreateShiftDetailsState state, BuildContext context) {
|
||||
state.suggestions.removeWhere(
|
||||
(e) => e.description == state.shift.fullAddress?.formattedAddress);
|
||||
return KwSuggestionInput(
|
||||
key: ValueKey('address_key${widget.id}'),
|
||||
title: 'Address',
|
||||
hintText: 'Hub address',
|
||||
horizontalPadding: 28,
|
||||
initialText: state.shift.fullAddress?.formattedAddress,
|
||||
items: state.suggestions,
|
||||
onQueryChanged: (query) {
|
||||
context
|
||||
.read<CreateShiftDetailsBloc>()
|
||||
.add(CreateShiftAddressQueryChangedEvent(query));
|
||||
},
|
||||
itemToStringBuilder: (item) => item.description,
|
||||
onSelected: (item) {
|
||||
context
|
||||
.read<CreateShiftDetailsBloc>()
|
||||
.add(CreateShiftAddressSelectEvent(place: item));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/features/create_event/domain/bloc/create_event_bloc.dart';
|
||||
import 'package:krow/features/create_event/presentation/create_shift_details_section/create_shift_widget.dart';
|
||||
|
||||
class CreateShiftsList extends StatefulWidget {
|
||||
const CreateShiftsList({super.key});
|
||||
|
||||
@override
|
||||
State<CreateShiftsList> createState() => _CreateShiftsListState();
|
||||
}
|
||||
|
||||
class _CreateShiftsListState extends State<CreateShiftsList> {
|
||||
final Map<String, GlobalKey> shiftKeys = {};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CreateEventBloc, CreateEventState>(
|
||||
builder: (context, state) {
|
||||
return SliverList.builder(
|
||||
itemCount: state.shifts.length,
|
||||
itemBuilder: (context, index) {
|
||||
final shiftId = state.shifts[index].id;
|
||||
if (shiftKeys[shiftId] == null) {
|
||||
shiftKeys[shiftId] = GlobalKey();
|
||||
}
|
||||
return BlocProvider.value(
|
||||
value: state.shifts[index].bloc,
|
||||
child: CreateShiftDetailsWidget(
|
||||
key: shiftKeys[shiftId],
|
||||
id: state.shifts[index].id,
|
||||
expanded: state.shifts[index].bloc.state.isExpanded,
|
||||
index: index),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/data/models/event/business_member_model.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
|
||||
class SelectContactPopup extends StatefulWidget {
|
||||
final List<BusinessMemberModel> contacts;
|
||||
final void Function(BusinessMemberModel contact)? onSelect;
|
||||
final Widget child;
|
||||
|
||||
const SelectContactPopup(
|
||||
{super.key,
|
||||
required this.contacts,
|
||||
required this.onSelect,
|
||||
required this.child});
|
||||
|
||||
@override
|
||||
_SelectContactPopupState createState() => _SelectContactPopupState();
|
||||
}
|
||||
|
||||
class _SelectContactPopupState extends State<SelectContactPopup> {
|
||||
OverlayEntry? _overlayEntry;
|
||||
List<BusinessMemberModel> _filteredItems = [];
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
final TextEditingController _controller = TextEditingController();
|
||||
final GlobalKey _childKey = GlobalKey();
|
||||
double? childY;
|
||||
|
||||
StateSetter? overlaySetState;
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _showPopup(BuildContext context) {
|
||||
_filteredItems = List.from(widget.contacts);
|
||||
|
||||
_overlayEntry = OverlayEntry(
|
||||
builder: (context) {
|
||||
return StatefulBuilder(builder: (context, setState) {
|
||||
overlaySetState = setState;
|
||||
return Stack(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: _hidePopup,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(color: Colors.transparent),
|
||||
),
|
||||
if (childY != null)
|
||||
Positioned(
|
||||
height:
|
||||
min(320, 82 + (44 * _filteredItems.length).toDouble()),
|
||||
top: childY,
|
||||
left: 28,
|
||||
right: 28,
|
||||
child: Material(
|
||||
elevation: 4,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: _buildPopupContent(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
Overlay.of(context).insert(_overlayEntry!);
|
||||
}
|
||||
|
||||
Widget _buildPopupContent() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: AppColors.grayTintStroke),
|
||||
),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
height: 40,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.grayPrimaryFrame,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Assets.images.icons.magnifyingGlass.svg(),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
style: AppTextStyles.bodyLargeMed,
|
||||
decoration: const InputDecoration(
|
||||
contentPadding: EdgeInsets.only(bottom: 8),
|
||||
hintText: 'Search',
|
||||
hintStyle: AppTextStyles.bodyLargeMed,
|
||||
border: InputBorder.none,
|
||||
),
|
||||
onChanged: _filterItems,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
height: min(238, (44 * _filteredItems.length).toDouble()),
|
||||
child: RawScrollbar(
|
||||
controller: _scrollController,
|
||||
thumbVisibility: true,
|
||||
padding: const EdgeInsets.only(right: 4),
|
||||
thumbColor: AppColors.grayDisable,
|
||||
trackColor: AppColors.buttonTertiaryActive,
|
||||
trackVisibility: true,
|
||||
radius: const Radius.circular(20),
|
||||
thickness: 4,
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
itemCount: _filteredItems.length,
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
itemBuilder: (context, index) {
|
||||
var item = _filteredItems[index];
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
widget.onSelect?.call(item);
|
||||
_hidePopup();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'${item.firstName} ${item.lastName}',
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
item.authInfo?.phone ?? '',
|
||||
style: AppTextStyles.bodySmallMed
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
final RenderBox renderBox =
|
||||
_childKey.currentContext!.findRenderObject() as RenderBox;
|
||||
final position = renderBox.localToGlobal(Offset.zero);
|
||||
childY = position.dy;
|
||||
_showPopup(context);
|
||||
},
|
||||
key: _childKey,
|
||||
child: widget.child);
|
||||
}
|
||||
|
||||
void _hidePopup() {
|
||||
_overlayEntry?.remove();
|
||||
_overlayEntry = null;
|
||||
_filteredItems = List.from(widget.contacts);
|
||||
_controller.clear();
|
||||
}
|
||||
|
||||
void _filterItems(String query) {
|
||||
overlaySetState?.call(() {
|
||||
_filteredItems = widget.contacts.where((item) {
|
||||
return ('${item.firstName}${item.lastName}${item.authInfo?.phone ?? ''}')
|
||||
.toLowerCase()
|
||||
.contains(query.toLowerCase());
|
||||
}).toList();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/data/models/event/business_member_model.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/features/create_event/presentation/create_shift_details_section/bloc/create_shift_details_bloc.dart';
|
||||
import 'package:krow/features/create_event/presentation/create_shift_details_section/widgets/select_contact_popup.dart';
|
||||
|
||||
class ShiftContactsWidget extends StatelessWidget {
|
||||
final List<BusinessMemberModel> allContacts;
|
||||
final List<BusinessMemberModel> selectedContacts;
|
||||
|
||||
const ShiftContactsWidget(
|
||||
{super.key, required this.allContacts, required this.selectedContacts});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
if (selectedContacts.isEmpty) {
|
||||
return buildSelectContactPopup(context);
|
||||
}
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(top: 12, left: 12, right: 12),
|
||||
margin: const EdgeInsets.only(top: 12),
|
||||
decoration: KwBoxDecorations.primaryLight12,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Shift Contact',
|
||||
style: AppTextStyles.bodyTinyReg
|
||||
.copyWith(color: AppColors.blackGray)),
|
||||
for (var item in selectedContacts)
|
||||
_buildContactItem(context, item, selectedContacts),
|
||||
buildSelectContactPopup(context),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
Widget buildSelectContactPopup(BuildContext context) {
|
||||
return SelectContactPopup(
|
||||
key: ObjectKey(selectedContacts),
|
||||
contacts: allContacts
|
||||
.where((element) => !selectedContacts.contains(element))
|
||||
.toList(),
|
||||
onSelect: (contact) {
|
||||
BlocProvider.of<CreateShiftDetailsBloc>(context)
|
||||
.add(CreateShiftSelectContactEvent(contact));
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 12, bottom: 12),
|
||||
height: 20,
|
||||
child: Row(
|
||||
children: [
|
||||
Assets.images.icons.profileAdd.svg(),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'Shift Contact',
|
||||
style: AppTextStyles.bodyMediumMed
|
||||
.copyWith(color: AppColors.primaryBlue),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Container _buildContactItem(BuildContext context, BusinessMemberModel item,
|
||||
List<BusinessMemberModel> selectedContacts) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
height: 36,
|
||||
width: 36,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(18), color: Colors.grey),
|
||||
),
|
||||
const Gap(8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${item.firstName} ${item.lastName}',
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
),
|
||||
const Gap(2),
|
||||
Text(
|
||||
item.authInfo?.phone ?? '',
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
SelectContactPopup(
|
||||
key: ObjectKey(selectedContacts),
|
||||
contacts: allContacts
|
||||
.where((element) => !selectedContacts.contains(element))
|
||||
.toList(),
|
||||
onSelect: (contact) {
|
||||
BlocProvider.of<CreateShiftDetailsBloc>(context)
|
||||
.add(CreateShiftRemoveContactEvent(item));
|
||||
|
||||
BlocProvider.of<CreateShiftDetailsBloc>(context)
|
||||
.add(CreateShiftSelectContactEvent(contact));
|
||||
},
|
||||
child: Container(
|
||||
height: 34,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.grayWhite,
|
||||
borderRadius: BorderRadius.circular(17),
|
||||
border: Border.all(color: AppColors.grayTintStroke)),
|
||||
child: Row(
|
||||
children: [
|
||||
Center(
|
||||
child: Assets.images.icons.edit.svg(
|
||||
width: 16,
|
||||
height: 16,
|
||||
colorFilter: const ColorFilter.mode(
|
||||
AppColors.blackBlack, BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
const Text(
|
||||
'Edit',
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
BlocProvider.of<CreateShiftDetailsBloc>(context)
|
||||
.add(CreateShiftRemoveContactEvent(item));
|
||||
},
|
||||
child: Container(
|
||||
height: 34,
|
||||
width: 34,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.grayWhite,
|
||||
borderRadius: BorderRadius.circular(17),
|
||||
border: Border.all(color: AppColors.grayTintStroke)),
|
||||
child: Center(
|
||||
child: Assets.images.icons.delete.svg(
|
||||
width: 16,
|
||||
height: 16,
|
||||
colorFilter: const ColorFilter.mode(
|
||||
AppColors.statusError, BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:krow/core/data/models/event/event_model.dart';
|
||||
import 'package:krow/core/entity/event_entity.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
part 'date_selector_event.dart';
|
||||
part 'date_selector_state.dart';
|
||||
|
||||
class DateSelectorBloc extends Bloc<DateSelectorEvent, DateSelectorState> {
|
||||
DateSelectorBloc() : super(const DateSelectorState()) {
|
||||
on<DateSelectorEventInit>(_onInit);
|
||||
on<DateSelectorEventChangeStartDate>(_onChangeStartDate);
|
||||
on<DateSelectorEventChangeEndDate>(_onChangeEndDate);
|
||||
on<DateSelectorEventRepeatType>(_onChangeRepeatType);
|
||||
on<DateSelectorEventEndType>(_onChangeEndType);
|
||||
on<DateSelectorEventChangeEndsAfterWeeks>(_onChangeEndsAfterWeeks);
|
||||
}
|
||||
|
||||
void _onInit(DateSelectorEventInit event, Emitter<DateSelectorState> emit) {
|
||||
if (event.eventModel != null) {
|
||||
emit(state.copyWith(
|
||||
startDate: DateTime.parse(event.eventModel!.date),
|
||||
));
|
||||
return;
|
||||
} else {
|
||||
emit(state.copyWith(
|
||||
recurringType: event.recurringType,
|
||||
endDateType: EndDateType.endDate,
|
||||
repeatType: EventRepeatType.daily,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void _onChangeStartDate(
|
||||
DateSelectorEventChangeStartDate event, Emitter<DateSelectorState> emit) {
|
||||
if (event.startDate == state.startDate) return;
|
||||
|
||||
event.entity.startDate = event.startDate;
|
||||
|
||||
emit(state.copyWith(
|
||||
startDate: event.startDate,
|
||||
));
|
||||
}
|
||||
|
||||
void _onChangeEndDate(
|
||||
DateSelectorEventChangeEndDate event, Emitter<DateSelectorState> emit) {
|
||||
if (event.endDate == state.startDate) return;
|
||||
event.entity.endDate = event.endDate;
|
||||
|
||||
emit(state.copyWith(
|
||||
endDate: event.endDate,
|
||||
));
|
||||
}
|
||||
|
||||
void _onChangeRepeatType(
|
||||
DateSelectorEventRepeatType event, Emitter<DateSelectorState> emit) {
|
||||
if (event.type == state.repeatType) return;
|
||||
emit(state.copyWith(
|
||||
repeatType: event.type,
|
||||
));
|
||||
}
|
||||
|
||||
void _onChangeEndType(
|
||||
DateSelectorEventEndType event, Emitter<DateSelectorState> emit) {
|
||||
if (event.type == state.endDateType) return;
|
||||
emit(state.copyWith(
|
||||
endDateType: event.type,
|
||||
));
|
||||
}
|
||||
|
||||
void _onChangeEndsAfterWeeks(DateSelectorEventChangeEndsAfterWeeks event,
|
||||
Emitter<DateSelectorState> emit) {
|
||||
if (event.weeks == state.andsAfterWeeksCount) return;
|
||||
emit(state.copyWith(
|
||||
andsAfterWeeksCount: event.weeks,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
part of 'date_selector_bloc.dart';
|
||||
|
||||
@immutable
|
||||
sealed class DateSelectorEvent {}
|
||||
|
||||
class DateSelectorEventInit extends DateSelectorEvent {
|
||||
final EventScheduleType? recurringType;
|
||||
final EventModel? eventModel;
|
||||
|
||||
DateSelectorEventInit(this.recurringType, this.eventModel);
|
||||
}
|
||||
|
||||
class DateSelectorEventChangeStartDate extends DateSelectorEvent {
|
||||
final DateTime startDate;
|
||||
final EventEntity entity;
|
||||
|
||||
DateSelectorEventChangeStartDate(this.startDate, this.entity);
|
||||
}
|
||||
|
||||
class DateSelectorEventChangeEndDate extends DateSelectorEvent {
|
||||
final DateTime endDate;
|
||||
final EventEntity entity;
|
||||
|
||||
|
||||
DateSelectorEventChangeEndDate(this.endDate, this.entity);
|
||||
}
|
||||
|
||||
class DateSelectorEventRepeatType extends DateSelectorEvent {
|
||||
final EventRepeatType type;
|
||||
|
||||
DateSelectorEventRepeatType(this.type);
|
||||
}
|
||||
|
||||
class DateSelectorEventEndType extends DateSelectorEvent {
|
||||
final EndDateType type;
|
||||
|
||||
DateSelectorEventEndType(this.type);
|
||||
}
|
||||
|
||||
class DateSelectorEventChangeEndsAfterWeeks extends DateSelectorEvent {
|
||||
final int weeks;
|
||||
|
||||
DateSelectorEventChangeEndsAfterWeeks(this.weeks);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
part of 'date_selector_bloc.dart';
|
||||
|
||||
enum EndDateType { endDate, endsAfter }
|
||||
|
||||
enum EventRepeatType {
|
||||
weekly,
|
||||
daily,
|
||||
}
|
||||
|
||||
@immutable
|
||||
class DateSelectorState {
|
||||
final DateTime? startDate;
|
||||
final DateTime? endDate;
|
||||
final EventScheduleType? recurringType;
|
||||
final EndDateType? endDateType;
|
||||
final EventRepeatType? repeatType;
|
||||
final int andsAfterWeeksCount;
|
||||
|
||||
const DateSelectorState(
|
||||
{this.startDate,
|
||||
this.endDate,
|
||||
this.recurringType = EventScheduleType.oneTime,
|
||||
this.endDateType,
|
||||
this.repeatType,
|
||||
this.andsAfterWeeksCount = 1});
|
||||
|
||||
DateSelectorState copyWith({
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
EventScheduleType? recurringType,
|
||||
EndDateType? endDateType,
|
||||
EventRepeatType? repeatType,
|
||||
int? andsAfterWeeksCount,
|
||||
}) {
|
||||
return DateSelectorState(
|
||||
startDate: startDate ?? this.startDate,
|
||||
endDate: endDate ?? this.endDate,
|
||||
recurringType: recurringType ?? this.recurringType,
|
||||
endDateType: endDateType ?? this.endDateType,
|
||||
repeatType: repeatType ?? this.repeatType,
|
||||
andsAfterWeeksCount: andsAfterWeeksCount ?? this.andsAfterWeeksCount,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
import 'package:calendar_date_picker2/calendar_date_picker2.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';
|
||||
|
||||
class CreateEventCalendar extends StatefulWidget {
|
||||
final DateTime? initialDate;
|
||||
|
||||
final void Function(DateTime value) onDateSelected;
|
||||
|
||||
const CreateEventCalendar(
|
||||
{super.key, required this.initialDate, required this.onDateSelected});
|
||||
|
||||
@override
|
||||
State<CreateEventCalendar> createState() => _CreateEventCalendarState();
|
||||
}
|
||||
|
||||
class _CreateEventCalendarState extends State<CreateEventCalendar> {
|
||||
final monthStr = [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec'
|
||||
];
|
||||
|
||||
final selectedTextStyle = AppTextStyles.bodyMediumSmb.copyWith(
|
||||
color: AppColors.grayWhite,
|
||||
);
|
||||
|
||||
final dayTextStyle = AppTextStyles.bodyMediumReg.copyWith(
|
||||
color: AppColors.bgColorDark,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CalendarDatePicker2(
|
||||
value: widget.initialDate == null ? [] : [widget.initialDate],
|
||||
config: CalendarDatePicker2Config(
|
||||
hideMonthPickerDividers: false,
|
||||
modePickersGap: 0,
|
||||
controlsHeight: 80,
|
||||
calendarType: CalendarDatePicker2Type.single,
|
||||
selectedRangeHighlightColor: AppColors.tintGray,
|
||||
selectedDayTextStyle: selectedTextStyle,
|
||||
dayTextStyle: dayTextStyle,
|
||||
selectedMonthTextStyle: selectedTextStyle,
|
||||
selectedYearTextStyle: selectedTextStyle,
|
||||
selectedDayHighlightColor: AppColors.bgColorDark,
|
||||
centerAlignModePicker: true,
|
||||
monthTextStyle: dayTextStyle,
|
||||
weekdayLabelBuilder: _dayWeekBuilder,
|
||||
dayBuilder: _dayBuilder,
|
||||
monthBuilder: _monthBuilder,
|
||||
yearBuilder: _yearBuilder,
|
||||
controlsTextStyle: AppTextStyles.headingH3,
|
||||
nextMonthIcon: Assets.images.icons.caretRight.svg(width: 24),
|
||||
lastMonthIcon: Assets.images.icons.caretLeft.svg(width: 24),
|
||||
customModePickerIcon: Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: Assets.images.icons.caretDown.svg(),
|
||||
),
|
||||
|
||||
// modePickerBuilder: _controlBuilder,
|
||||
),
|
||||
onValueChanged: (dates) {
|
||||
widget.onDateSelected.call(dates.first);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _monthBuilder({
|
||||
required int month,
|
||||
TextStyle? textStyle,
|
||||
BoxDecoration? decoration,
|
||||
bool? isSelected,
|
||||
bool? isDisabled,
|
||||
bool? isCurrentMonth,
|
||||
}) {
|
||||
return Center(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 16),
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected == true ? AppColors.bgColorDark : null,
|
||||
borderRadius: BorderRadius.circular(23),
|
||||
border: Border.all(
|
||||
color: AppColors.grayStroke,
|
||||
width: isSelected == true ? 0 : 1,
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
monthStr[month - 1],
|
||||
style: isSelected == true
|
||||
? AppTextStyles.bodyMediumMed
|
||||
.copyWith(color: AppColors.grayWhite)
|
||||
: AppTextStyles.bodyMediumReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _yearBuilder({
|
||||
required int year,
|
||||
TextStyle? textStyle,
|
||||
BoxDecoration? decoration,
|
||||
bool? isSelected,
|
||||
bool? isDisabled,
|
||||
bool? isCurrentYear,
|
||||
}) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 12),
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected == true ? AppColors.bgColorDark : null,
|
||||
borderRadius: BorderRadius.circular(23),
|
||||
border: Border.all(
|
||||
color: AppColors.grayStroke,
|
||||
width: isSelected == true ? 0 : 1,
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
year.toString(),
|
||||
style: isSelected == true
|
||||
? AppTextStyles.bodyMediumMed.copyWith(color: AppColors.grayWhite)
|
||||
: AppTextStyles.bodyMediumReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _dayBuilder({
|
||||
required DateTime date,
|
||||
TextStyle? textStyle,
|
||||
BoxDecoration? decoration,
|
||||
bool? isSelected,
|
||||
bool? isDisabled,
|
||||
bool? isToday,
|
||||
}) {
|
||||
bool past = _isPast(date);
|
||||
var dayDecoration = BoxDecoration(
|
||||
color: isSelected == true ? AppColors.bgColorDark : null,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
);
|
||||
var dayTextStyle = AppTextStyles.bodyMediumReg.copyWith(
|
||||
color: past ? AppColors.blackCaptionText : AppColors.bgColorDark,
|
||||
);
|
||||
return Center(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(left: 2, right: 2),
|
||||
alignment: Alignment.center,
|
||||
decoration: dayDecoration,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
date.day.toString(),
|
||||
style: isSelected == true ? selectedTextStyle : dayTextStyle,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const Gap(2),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool _isPast(DateTime date) {
|
||||
var now = DateTime.now();
|
||||
var nowOnly = DateTime(now.year, now.month, now.day);
|
||||
var past = date.isBefore(nowOnly);
|
||||
return past;
|
||||
}
|
||||
|
||||
Widget? _dayWeekBuilder({
|
||||
required int weekday,
|
||||
bool? isScrollViewTopHeader,
|
||||
}) {
|
||||
return Text(
|
||||
['S', 'M', 'T', 'W', 'T', 'F', 'S'][weekday],
|
||||
style: AppTextStyles.bodyMediumSmb.copyWith(
|
||||
color: AppColors.bgColorDark,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
|
||||
import 'package:krow/features/create_event/presentation/event_date_section/create_event_calendar_widget.dart';
|
||||
|
||||
class CreateEventDatePopup extends StatefulWidget {
|
||||
final DateTime? initDate;
|
||||
|
||||
const CreateEventDatePopup({
|
||||
super.key,
|
||||
this.initDate,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CreateEventDatePopup> createState() => _CreateEventDatePopupState();
|
||||
|
||||
static Future<DateTime?> show(
|
||||
{required BuildContext context, DateTime? initDate}) async {
|
||||
return showDialog<DateTime>(
|
||||
context: context,
|
||||
builder: (context) => CreateEventDatePopup(
|
||||
initDate: initDate,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CreateEventDatePopupState extends State<CreateEventDatePopup> {
|
||||
DateTime? selectedDate;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
selectedDate = widget.initDate;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
margin: const EdgeInsets.all(16),
|
||||
decoration: KwBoxDecorations.white24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
buildHeader(),
|
||||
const Gap(24),
|
||||
_buildCalendar(),
|
||||
const Gap(24),
|
||||
..._buttonGroup(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Row buildHeader() {
|
||||
return const Row(
|
||||
children: [
|
||||
Text('Select Date from Calendar', style: AppTextStyles.headingH3),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buttonGroup() {
|
||||
return [
|
||||
KwButton.primary(
|
||||
disabled: selectedDate == null,
|
||||
label: 'Pick Date',
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(selectedDate);
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
KwButton.outlinedPrimary(
|
||||
label: 'Cancel',
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(widget.initDate);
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
_buildCalendar() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.grayPrimaryFrame,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColors.grayTintStroke),
|
||||
),
|
||||
child: CreateEventCalendar(
|
||||
onDateSelected: (date) {
|
||||
setState(() {
|
||||
selectedDate = date;
|
||||
});
|
||||
},
|
||||
initialDate: selectedDate ?? widget.initDate,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
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/data/models/event/event_model.dart';
|
||||
import 'package:krow/core/entity/event_entity.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_dropdown.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_option_selector.dart';
|
||||
import 'package:krow/features/create_event/domain/bloc/create_event_bloc.dart';
|
||||
import 'package:krow/features/create_event/presentation/event_date_section/bloc/date_selector_bloc.dart';
|
||||
import 'package:krow/features/create_event/presentation/event_date_section/create_event_date_popup.dart';
|
||||
|
||||
class EventDateInputWidget extends StatelessWidget {
|
||||
final EventEntity entity;
|
||||
|
||||
const EventDateInputWidget(this.entity, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<DateSelectorBloc, DateSelectorState>(
|
||||
listener: (context, state) {
|
||||
BlocProvider.of<CreateEventBloc>(context)
|
||||
.add(CreateEventEntityUpdatedEvent());
|
||||
},
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: state.recurringType == EventScheduleType.oneTime
|
||||
? ([
|
||||
_buildDateInput(context, 'Date', state.startDate, (newDate) {
|
||||
BlocProvider.of<DateSelectorBloc>(context)
|
||||
.add(DateSelectorEventChangeStartDate(newDate, entity));
|
||||
}),
|
||||
])
|
||||
: [
|
||||
const Gap(16),
|
||||
const Divider(
|
||||
color: AppColors.grayTintStroke,
|
||||
thickness: 1,
|
||||
height: 0,
|
||||
),
|
||||
_buildDateInput(context, 'Start Date', state.startDate,
|
||||
(newDate) {
|
||||
BlocProvider.of<DateSelectorBloc>(context)
|
||||
.add(DateSelectorEventChangeStartDate(newDate, entity));
|
||||
}),
|
||||
..._buildRepeatSelector(state, context),
|
||||
...buildEndTypeSelector(state, context),
|
||||
AnimatedCrossFade(
|
||||
firstChild: _buildDateInput(
|
||||
context, 'End Date', state.endDate, (newDate) {
|
||||
BlocProvider.of<DateSelectorBloc>(context)
|
||||
.add(DateSelectorEventChangeEndDate(newDate, entity));
|
||||
}),
|
||||
secondChild: _buildEndAfterDropdown(state, context),
|
||||
crossFadeState: state.endDateType == EndDateType.endDate
|
||||
? CrossFadeState.showFirst
|
||||
: CrossFadeState.showSecond,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
),
|
||||
const Gap(16),
|
||||
const Divider(
|
||||
color: AppColors.grayTintStroke,
|
||||
thickness: 1,
|
||||
height: 0,
|
||||
),
|
||||
const Gap(8),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEndAfterDropdown(DateSelectorState state, BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: KwDropdown<int>(
|
||||
selectedItem: KwDropDownItem(
|
||||
data: state.andsAfterWeeksCount,
|
||||
title: '${state.andsAfterWeeksCount} Weeks'),
|
||||
hintText: '',
|
||||
title: 'After',
|
||||
horizontalPadding: 28,
|
||||
onSelected: (int item) {
|
||||
BlocProvider.of<DateSelectorBloc>(context)
|
||||
.add(DateSelectorEventChangeEndsAfterWeeks(item));
|
||||
},
|
||||
items: const [
|
||||
KwDropDownItem(data: 1, title: '1 Weeks'),
|
||||
KwDropDownItem(data: 2, title: '2 Weeks'),
|
||||
KwDropDownItem(data: 3, title: '3 Weeks'),
|
||||
KwDropDownItem(data: 5, title: '4 Weeks'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildRepeatSelector(
|
||||
DateSelectorState state, BuildContext context) {
|
||||
return [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16, top: 8, bottom: 4),
|
||||
child: Text(
|
||||
'Repeat',
|
||||
style: AppTextStyles.bodyTinyReg.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
),
|
||||
KwOptionSelector(
|
||||
selectedIndex: state.repeatType?.index,
|
||||
backgroundColor: AppColors.grayPrimaryFrame,
|
||||
onChanged: (index) {
|
||||
BlocProvider.of<DateSelectorBloc>(context).add(
|
||||
DateSelectorEventRepeatType(EventRepeatType.values[index]));
|
||||
},
|
||||
items: const [
|
||||
'Weekly',
|
||||
'Daily',
|
||||
])
|
||||
];
|
||||
}
|
||||
|
||||
Widget _buildDateInput(BuildContext context, String title, DateTime? date,
|
||||
Function(DateTime date) onSelect) {
|
||||
var formattedDate = date == null
|
||||
? 'mm.dd.yyyy'
|
||||
: DateFormat('MM.dd.yyyy').format(date).toLowerCase();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16, top: 8),
|
||||
child: Text(
|
||||
'Date',
|
||||
style:
|
||||
AppTextStyles.bodyTinyReg.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
var newDate = await CreateEventDatePopup.show(
|
||||
context: context, initDate: date);
|
||||
if (newDate != null && context.mounted) {
|
||||
onSelect(newDate);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
height: 48,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: AppColors.grayStroke),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(formattedDate,
|
||||
style: AppTextStyles.bodyMediumReg.copyWith(
|
||||
color: date != null ? null : AppColors.blackGray)),
|
||||
const Gap(10),
|
||||
Assets.images.icons.calendar.svg(width: 16, height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> buildEndTypeSelector(
|
||||
DateSelectorState state, BuildContext context) {
|
||||
return [
|
||||
const Gap(24),
|
||||
KwOptionSelector(
|
||||
selectedIndex: state.endDateType?.index,
|
||||
onChanged: (index) {
|
||||
BlocProvider.of<DateSelectorBloc>(context)
|
||||
.add(DateSelectorEventEndType(EndDateType.values[index]));
|
||||
},
|
||||
height: 26,
|
||||
selectorHeight: 4,
|
||||
textStyle:
|
||||
AppTextStyles.bodyMediumReg.copyWith(color: AppColors.blackGray),
|
||||
selectedTextStyle: AppTextStyles.bodyMediumMed,
|
||||
itemAlign: Alignment.topCenter,
|
||||
items: const [
|
||||
'Ends on Date',
|
||||
'Ends After # Weeks',
|
||||
])
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:krow/core/entity/role_schedule_entity.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
part 'role_schedule_dialog_event.dart';
|
||||
part 'role_schedule_dialog_state.dart';
|
||||
|
||||
class RoleScheduleDialogBloc
|
||||
extends Bloc<RoleScheduleDialogEvent, RoleScheduleDialogState> {
|
||||
RoleScheduleDialogBloc(List<RoleScheduleEntity> schedule)
|
||||
: super(RoleScheduleDialogState(schedule: List.from(schedule))) {
|
||||
on<TapOnScheduleDayEvent>(_onTapOnScheduleDay);
|
||||
on<SetRoleScheduleStartTimeEvent>(_onSetRoleScheduleStartTime);
|
||||
on<SetRoleScheduleEndTimeEvent>(_onSetRoleScheduleEndTime);
|
||||
}
|
||||
|
||||
FutureOr<void> _onTapOnScheduleDay(TapOnScheduleDayEvent event, emit) {
|
||||
if (state.schedule.any((element) => element.dayIndex == event.index)) {
|
||||
emit(state.copyWith(
|
||||
schedule: state.schedule
|
||||
.where((element) => element.dayIndex != event.index)
|
||||
.toList()));
|
||||
} else {
|
||||
var today = DateTime.now();
|
||||
var defStartTime = DateTime(today.year, today.month, today.day, 9, 0);
|
||||
var defEndTime = DateTime(today.year, today.month, today.day, 18, 0);
|
||||
emit(state.copyWith(schedule: [
|
||||
...state.schedule,
|
||||
RoleScheduleEntity(
|
||||
dayIndex: event.index, startTime: defStartTime, endTime: defEndTime)
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
FutureOr<void> _onSetRoleScheduleStartTime(event, emit) {
|
||||
emit(state.copyWith(
|
||||
schedule: state.schedule
|
||||
.map((e) => e.dayIndex == event.schedule.dayIndex
|
||||
? e.copyWith(startTime: event.startTime)
|
||||
: e)
|
||||
.toList()));
|
||||
}
|
||||
|
||||
FutureOr<void> _onSetRoleScheduleEndTime(event, emit) {
|
||||
emit(state.copyWith(
|
||||
schedule: state.schedule
|
||||
.map((e) => e.dayIndex == event.schedule.dayIndex
|
||||
? e.copyWith(endTime: event.endTime)
|
||||
: e)
|
||||
.toList()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
part of 'role_schedule_dialog_bloc.dart';
|
||||
|
||||
@immutable
|
||||
sealed class RoleScheduleDialogEvent {}
|
||||
|
||||
class TapOnScheduleDayEvent extends RoleScheduleDialogEvent {
|
||||
final int index;
|
||||
|
||||
TapOnScheduleDayEvent(this.index);
|
||||
}
|
||||
|
||||
class SetRoleScheduleStartTimeEvent extends RoleScheduleDialogEvent {
|
||||
final RoleScheduleEntity schedule;
|
||||
final DateTime startTime;
|
||||
|
||||
SetRoleScheduleStartTimeEvent(this.schedule, this.startTime);
|
||||
}
|
||||
|
||||
class SetRoleScheduleEndTimeEvent extends RoleScheduleDialogEvent {
|
||||
final RoleScheduleEntity schedule;
|
||||
final DateTime endTime;
|
||||
|
||||
SetRoleScheduleEndTimeEvent(this.schedule, this.endTime);
|
||||
}
|
||||
|
||||
class SaveRoleScheduleEvent extends RoleScheduleDialogEvent {
|
||||
SaveRoleScheduleEvent();
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
part of 'role_schedule_dialog_bloc.dart';
|
||||
|
||||
@immutable
|
||||
class RoleScheduleDialogState {
|
||||
final List<RoleScheduleEntity> schedule;
|
||||
|
||||
const RoleScheduleDialogState({required this.schedule});
|
||||
|
||||
RoleScheduleDialogState copyWith({List<RoleScheduleEntity>? schedule}) {
|
||||
return RoleScheduleDialogState(
|
||||
schedule: schedule ?? this.schedule,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/application/common/int_extensions.dart';
|
||||
import 'package:krow/core/entity/role_schedule_entity.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/kw_time_slot.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
|
||||
import 'package:krow/features/create_event/presentation/role_schedule_dialog/bloc/role_schedule_dialog_bloc.dart';
|
||||
|
||||
class RecurringScheduleDialog extends StatefulWidget {
|
||||
const RecurringScheduleDialog({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<RecurringScheduleDialog> createState() =>
|
||||
_RecurringScheduleDialogState();
|
||||
|
||||
static Future<List<RoleScheduleEntity>?> show(
|
||||
BuildContext context, List<RoleScheduleEntity>? schedule) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return BlocProvider(
|
||||
create: (context) => RoleScheduleDialogBloc(schedule ?? []),
|
||||
child: const RecurringScheduleDialog(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RecurringScheduleDialogState extends State<RecurringScheduleDialog> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<RoleScheduleDialogBloc, RoleScheduleDialogState>(
|
||||
builder: (context, state) {
|
||||
return Center(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.symmetric(vertical: 24),
|
||||
decoration: KwBoxDecorations.white24,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildHeader(context),
|
||||
const Gap(24),
|
||||
_buildDayTabs(state.schedule),
|
||||
const Gap(24),
|
||||
...state.schedule.map((e) => _buildTimeSlots(e)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: KwButton.primary(
|
||||
label: 'Save Slots',
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(state.schedule);
|
||||
},
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: KwButton.outlinedPrimary(
|
||||
label: 'Cancel',
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Event Schedule',
|
||||
style: AppTextStyles.bodyLargeMed,
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Assets.images.icons.x.svg(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 24),
|
||||
child: Text(
|
||||
'Select the days and time slots that you want this schedule to recur on',
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_buildDayTabs(List<RoleScheduleEntity> schedule) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(left: 24, right: 20),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
||||
.indexed
|
||||
.map((e) {
|
||||
var (index, item) = e;
|
||||
var selected = schedule.any((element) => element.dayIndex == index);
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
BlocProvider.of<RoleScheduleDialogBloc>(context)
|
||||
.add(TapOnScheduleDayEvent(index));
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
margin: const EdgeInsets.only(right: 4),
|
||||
height: 46,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(23),
|
||||
color: selected ? AppColors.bgColorDark : Colors.transparent,
|
||||
border: Border.all(
|
||||
color: selected ? AppColors.bgColorDark : AppColors.grayStroke,
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
item,
|
||||
style: AppTextStyles.bodyMediumReg.copyWith(
|
||||
color: selected ? AppColors.grayWhite : AppColors.blackGray),
|
||||
)),
|
||||
),
|
||||
);
|
||||
}).toList()),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTimeSlots(RoleScheduleEntity e) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 24, right: 24, bottom: 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: e.dayIndex.getWeekdayId(),
|
||||
style: AppTextStyles.bodyMediumReg.copyWith(height: 1),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: ' Time Slot',
|
||||
style: AppTextStyles.bodyTinyReg
|
||||
.copyWith(color: AppColors.blackGray, height: 1),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
_timeRow(context, e),
|
||||
buildScheduleButton(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_timeRow(context, RoleScheduleEntity entity) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: KwTimeSlotInput(
|
||||
key: ValueKey('dialog_start_time_key_${entity.dayIndex}'),
|
||||
label: 'Start Time',
|
||||
initialValue: entity.startTime,
|
||||
onChange: (value) {
|
||||
BlocProvider.of<RoleScheduleDialogBloc>(context)
|
||||
.add(SetRoleScheduleStartTimeEvent(entity, value));
|
||||
},
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: KwTimeSlotInput(
|
||||
key: ValueKey('dialog_start_time_key_${entity.dayIndex}'),
|
||||
label: 'End Time',
|
||||
initialValue: entity.endTime,
|
||||
onChange: (value) {
|
||||
BlocProvider.of<RoleScheduleDialogBloc>(context)
|
||||
.add(SetRoleScheduleEndTimeEvent(entity, value));
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
GestureDetector buildScheduleButton() {
|
||||
return GestureDetector(
|
||||
onTap: () {},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 12),
|
||||
height: 20,
|
||||
child: Row(
|
||||
children: [
|
||||
Assets.images.icons.copy.svg(),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'Copy Time to All',
|
||||
style: AppTextStyles.bodyMediumMed
|
||||
.copyWith(color: AppColors.primaryBlue),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/application/common/int_extensions.dart';
|
||||
import 'package:krow/core/entity/role_schedule_entity.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/features/create_event/presentation/create_role_section/bloc/create_role_bloc.dart';
|
||||
import 'package:krow/features/create_event/presentation/role_schedule_dialog/recurring_schedule_dialog.dart';
|
||||
|
||||
class RecurringScheduleWidget extends StatelessWidget {
|
||||
const RecurringScheduleWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CreateRoleBloc, CreateRoleState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (state.entity.schedule != null &&
|
||||
(state.entity.schedule?.isNotEmpty ?? false)) ...[
|
||||
const Gap(12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: const Text(
|
||||
'Schedule',
|
||||
style: AppTextStyles.bodyMediumReg,
|
||||
),
|
||||
),
|
||||
...state.entity.schedule!.map((e) => _buildScheduleItem(e)),
|
||||
const Gap(8),
|
||||
],
|
||||
buildScheduleButton(context, state),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
GestureDetector buildScheduleButton(
|
||||
BuildContext context, CreateRoleState state) {
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
var result =
|
||||
await RecurringScheduleDialog.show(context, state.entity.schedule);
|
||||
if (result != null) {
|
||||
context
|
||||
.read<CreateRoleBloc>()
|
||||
.add(CreateRoleSetScheduleEvent(result));
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 12),
|
||||
height: 20,
|
||||
child: Row(
|
||||
children: [
|
||||
Assets.images.icons.calendarEdit.svg(),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'Set Work Days',
|
||||
style: AppTextStyles.bodyMediumMed
|
||||
.copyWith(color: AppColors.primaryBlue),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildScheduleItem(RoleScheduleEntity e) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
height: 46,
|
||||
alignment: Alignment.centerLeft,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(23),
|
||||
border: Border.all(color: AppColors.grayStroke, width: 1),
|
||||
),
|
||||
child: Text(e.dayIndex.getWeekdayId()),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_input.dart';
|
||||
import 'package:krow/features/create_event/domain/bloc/create_event_bloc.dart';
|
||||
|
||||
class AddInfoInputWidget extends StatefulWidget {
|
||||
const AddInfoInputWidget({super.key});
|
||||
|
||||
@override
|
||||
State<AddInfoInputWidget> createState() => _AddInfoInputWidgetState();
|
||||
}
|
||||
|
||||
class _AddInfoInputWidgetState extends State<AddInfoInputWidget> {
|
||||
final TextEditingController _addInfoController = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<CreateEventBloc, CreateEventState>(
|
||||
listener: (context, state) {
|
||||
if (state.entity.additionalInfo != _addInfoController.text) {
|
||||
_addInfoController.text = state.entity.additionalInfo??'';
|
||||
}
|
||||
},
|
||||
buildWhen: (previous, current) {
|
||||
return previous.entity.additionalInfo != current.entity.additionalInfo;
|
||||
},
|
||||
builder: (context, state) {
|
||||
return KwTextInput(
|
||||
controller: _addInfoController,
|
||||
onChanged: (value) {
|
||||
BlocProvider.of<CreateEventBloc>(context)
|
||||
.add(CreateEventAddInfoChange(value));
|
||||
},
|
||||
maxLength: 300,
|
||||
showCounter: true,
|
||||
minHeight: 144,
|
||||
hintText: 'Enter your main text here...',
|
||||
title: 'Additional Information',
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'package:flutter/cupertino.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/features/create_event/domain/bloc/create_event_bloc.dart';
|
||||
|
||||
class AddonsSectionWidget extends StatelessWidget {
|
||||
const AddonsSectionWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CreateEventBloc, CreateEventState>(
|
||||
builder: (context, state) {
|
||||
var allAddons = state.addons;
|
||||
var selectedAddons = state.entity.addons;
|
||||
if(context.read<CreateEventBloc>().state.addons.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
const Divider(
|
||||
height: 0,
|
||||
color: AppColors.grayStroke,
|
||||
),
|
||||
for (var addon in allAddons)
|
||||
_addonStrokeItem(
|
||||
title: addon.name ?? '',
|
||||
enabled: selectedAddons
|
||||
?.any((selected) => selected.id == addon.id) ??
|
||||
false,
|
||||
onTap: () {
|
||||
BlocProvider.of<CreateEventBloc>(context)
|
||||
.add(CreateEventToggleAddon(addon));
|
||||
},
|
||||
),
|
||||
const Gap(12),
|
||||
const Divider(
|
||||
height: 0,
|
||||
color: AppColors.grayStroke,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
_addonStrokeItem(
|
||||
{required String title,
|
||||
required bool enabled,
|
||||
required VoidCallback onTap}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
)),
|
||||
CupertinoSwitch(
|
||||
value: enabled,
|
||||
onChanged: (_) {
|
||||
onTap();
|
||||
},
|
||||
activeTrackColor: AppColors.bgColorDark,
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/data/models/event/hub_model.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_dropdown.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_input.dart';
|
||||
import 'package:krow/features/create_event/domain/bloc/create_event_bloc.dart';
|
||||
import 'package:krow/features/create_event/presentation/event_date_section/event_date_input_widget.dart';
|
||||
|
||||
import '../../../../core/entity/event_entity.dart';
|
||||
|
||||
class CreateEventDetailsCardWidget extends StatefulWidget {
|
||||
const CreateEventDetailsCardWidget({super.key});
|
||||
|
||||
@override
|
||||
State<CreateEventDetailsCardWidget> createState() =>
|
||||
_CreateEventDetailsCardWidgetState();
|
||||
}
|
||||
|
||||
class _CreateEventDetailsCardWidgetState
|
||||
extends State<CreateEventDetailsCardWidget>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
TextEditingController poNumberController = TextEditingController();
|
||||
// TextEditingController contractNumberController = TextEditingController();
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return BlocConsumer<CreateEventBloc, CreateEventState>(
|
||||
listener: (context, state) {
|
||||
if (state.entity.name != _nameController.text) {
|
||||
_nameController.text = state.entity.name;
|
||||
}
|
||||
|
||||
// if (state.entity.contractNumber != null &&
|
||||
// state.entity.contractNumber != contractNumberController.text) {
|
||||
// contractNumberController.text = state.entity.contractNumber!;
|
||||
// }
|
||||
//
|
||||
if (state.entity.poNumber != null &&
|
||||
state.entity.poNumber != poNumberController.text) {
|
||||
poNumberController.text = state.entity.poNumber!;
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return AnimatedContainer(
|
||||
duration: Duration(milliseconds: 300),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 24),
|
||||
decoration: KwBoxDecorations.white12.copyWith(
|
||||
border: state.validationState != null
|
||||
? Border.all(color: AppColors.statusError, width: 1)
|
||||
: null),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Event Details',
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
),
|
||||
const Gap(12),
|
||||
KwTextInput(
|
||||
controller: _nameController,
|
||||
title: 'Event Name',
|
||||
hintText: 'Enter event name',
|
||||
onChanged: (value) {
|
||||
BlocProvider.of<CreateEventBloc>(context)
|
||||
.add(CreateEventNameChange(value));
|
||||
},
|
||||
),
|
||||
IgnorePointer(
|
||||
ignoring: ![EventStatus.draft, EventStatus.pending].contains(state.entity.status) && state.entity.status!=null,
|
||||
child: EventDateInputWidget(state.entity)),
|
||||
const Gap(8),
|
||||
_hubDropdown(state.hubs, state),
|
||||
const Gap(8),
|
||||
// _contractDropdown(context, state),
|
||||
// if (state.entity.contractType == EventContractType.contract)
|
||||
// _buildContractInput(),
|
||||
// if (state.entity.contractType == EventContractType.purchaseOrder)
|
||||
_buildPurchaseInput(),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Column _hubDropdown(List<HubModel> hubs, CreateEventState state) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
child: Text(
|
||||
'Location',
|
||||
style:
|
||||
AppTextStyles.bodyTinyReg.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
KwDropdown(
|
||||
horizontalPadding: 28,
|
||||
hintText: 'Hub name',
|
||||
selectedItem: state.entity.hub != null
|
||||
? KwDropDownItem(
|
||||
data: state.entity.hub, title: state.entity.hub?.name ?? '')
|
||||
: null,
|
||||
items: hubs
|
||||
.map((e) => KwDropDownItem(data: e, title: e.name ?? ''))
|
||||
.toList(),
|
||||
onSelected: (item) {
|
||||
BlocProvider.of<CreateEventBloc>(context)
|
||||
.add(CreateEventChangeHub(item!));
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Column _contractDropdown(BuildContext context, CreateEventState state) {
|
||||
// return Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// children: [
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.only(left: 16),
|
||||
// child: Text(
|
||||
// 'Payment type',
|
||||
// style:
|
||||
// AppTextStyles.bodyTinyReg.copyWith(color: AppColors.blackGray),
|
||||
// ),
|
||||
// ),
|
||||
// const Gap(4),
|
||||
// KwDropdown(
|
||||
// horizontalPadding: 28,
|
||||
// hintText: 'Direct',
|
||||
// selectedItem: KwDropDownItem(
|
||||
// data: state.entity.contractType,
|
||||
// title: state.entity.contractType.formattedName),
|
||||
// items: const [
|
||||
// KwDropDownItem(data: EventContractType.direct, title: 'Direct'),
|
||||
// KwDropDownItem(
|
||||
// data: EventContractType.purchaseOrder,
|
||||
// title: 'Purchase Order'),
|
||||
// KwDropDownItem(
|
||||
// data: EventContractType.contract, title: 'Contract'),
|
||||
// ],
|
||||
// onSelected: (item) {
|
||||
// BlocProvider.of<CreateEventBloc>(context)
|
||||
// .add(CreateEventChangeContractType(item));
|
||||
// }),
|
||||
// ],
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// _buildContractInput() {
|
||||
// return Padding(
|
||||
// padding: const EdgeInsets.only(top:8.0),
|
||||
// child: KwTextInput(
|
||||
// controller: contractNumberController,
|
||||
// onChanged: (value) {
|
||||
// BlocProvider.of<CreateEventBloc>(context)
|
||||
// .add(CreateEventChangeContractNumber(value));
|
||||
// },
|
||||
// title: 'Contract number',
|
||||
// hintText: '#00000'),
|
||||
// );
|
||||
// }
|
||||
//
|
||||
_buildPurchaseInput() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top:8.0),
|
||||
child: KwTextInput(
|
||||
controller: poNumberController,
|
||||
onChanged: (value) {
|
||||
BlocProvider.of<CreateEventBloc>(context)
|
||||
.add(CreateEventChangePoNumber(value));
|
||||
},
|
||||
title: 'PO Reference number',
|
||||
hintText: 'PO Reference number'),
|
||||
);
|
||||
}
|
||||
}
|
||||
//
|
||||
// extension on EventContractType? {
|
||||
// String get formattedName {
|
||||
// return switch (this) {
|
||||
// EventContractType.direct => 'Direct',
|
||||
// EventContractType.contract => 'Contract',
|
||||
// EventContractType.purchaseOrder => 'Purchase Order',
|
||||
// null => 'null'
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,136 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/data/models/event/tag_model.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/features/create_event/domain/bloc/create_event_bloc.dart';
|
||||
|
||||
class CreateEventTagsCard extends StatelessWidget {
|
||||
const CreateEventTagsCard({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CreateEventBloc, CreateEventState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.tags != current.tags ||
|
||||
previous.entity.tags != current.entity.tags,
|
||||
builder: (context, state) {
|
||||
if(state.tags.isEmpty) return const SizedBox();
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.only(top: 12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 24),
|
||||
decoration: KwBoxDecorations.white12,
|
||||
child: _buildChips(context, state.tags, state.entity.tags ?? []),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChips(BuildContext context, List<TagModel> allTags,
|
||||
List<TagModel> selectedTags) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 12),
|
||||
child: Wrap(
|
||||
runSpacing: 8,
|
||||
spacing: 8,
|
||||
children: allTags
|
||||
.map((e) =>
|
||||
_buildTag(context, e, selectedTags.any((t) => e.id == t.id)))
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTag(BuildContext context, TagModel tag, bool selected) {
|
||||
const duration = Duration(milliseconds: 150);
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
BlocProvider.of<CreateEventBloc>(context)
|
||||
.add(CreateEventTagSelected(tag));
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: duration,
|
||||
padding: const EdgeInsets.all(8),
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: selected ? AppColors.blackBlack : AppColors.grayTintStroke,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
duration: duration,
|
||||
height: 28,
|
||||
width: 28,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: selected ? AppColors.blackBlack : AppColors.tintGray,
|
||||
),
|
||||
child: Center(
|
||||
child: getImageById(tag.id).svg(
|
||||
width: 12.0,
|
||||
height: 12.0,
|
||||
colorFilter: ColorFilter.mode(
|
||||
selected ? AppColors.grayWhite : AppColors.blackBlack,
|
||||
BlendMode.srcIn)),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
tag.name,
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.bgColorDark),
|
||||
),
|
||||
const Gap(8),
|
||||
AnimatedContainer(
|
||||
duration: duration,
|
||||
height: 16,
|
||||
width: 16,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: selected
|
||||
? AppColors.blackBlack
|
||||
: AppColors.grayTintStroke,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: AnimatedContainer(
|
||||
duration: duration,
|
||||
height: 6,
|
||||
width: 6,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color:
|
||||
selected ? AppColors.blackBlack : Colors.transparent,
|
||||
)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
getImageById(String id) {
|
||||
switch (id) {
|
||||
case '1':
|
||||
return Assets.images.icons.tags.award;
|
||||
case '2':
|
||||
return Assets.images.icons.tags.briefcase;
|
||||
case '3':
|
||||
return Assets.images.icons.tags.flash;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
|
||||
class CreateEventTitleWidget extends StatelessWidget {
|
||||
const CreateEventTitleWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Gap(8),
|
||||
const Text('Create Your Event', style: AppTextStyles.headingH1),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'Bring your vision to life! Share details, set the stage, and connect with your audience—your event starts here.',
|
||||
style:
|
||||
AppTextStyles.bodyMediumReg.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
const Gap(24),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/features/create_event/domain/bloc/create_event_bloc.dart';
|
||||
|
||||
class TotalCostRowWidget extends StatelessWidget {
|
||||
TotalCostRowWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CreateEventBloc, CreateEventState>(
|
||||
builder: (context, state) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Approximate Total Costs',
|
||||
style: AppTextStyles.bodyMediumReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: state.entity.totalCost,
|
||||
builder: (context, value, child) {
|
||||
return Text(
|
||||
'\$${value.toStringAsFixed(2)}',
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import 'package:graphql_flutter/graphql_flutter.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:krow/core/application/clients/api/api_client.dart';
|
||||
import 'package:krow/core/application/clients/api/api_exception.dart';
|
||||
import 'package:krow/core/data/models/event/event_model.dart';
|
||||
import 'package:krow/core/data/models/pagination_wrapper/pagination_wrapper.dart';
|
||||
import 'package:krow/features/events/data/events_gql.dart';
|
||||
|
||||
@Injectable()
|
||||
class EventsApiProvider {
|
||||
final ApiClient _client;
|
||||
|
||||
EventsApiProvider({required ApiClient client}) : _client = client;
|
||||
|
||||
Future<PaginationWrapper?> fetchEvents(String status, {String? after}) async {
|
||||
final QueryResult result = await _client.query(
|
||||
schema: getEventsQuery,
|
||||
body: {'status': status, 'first': 30, 'after': after});
|
||||
if (result.hasException) {
|
||||
throw parseBackendError(result.exception);
|
||||
}
|
||||
|
||||
if (result.data == null || result.data!['client_events'] == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return PaginationWrapper<EventModel?>.fromJson(
|
||||
result.data!['client_events'], (json) {
|
||||
try {
|
||||
return EventModel.fromJson(json);
|
||||
}catch (e) {
|
||||
print(e);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<EventModel?> fetchEventById(String id) async {
|
||||
final QueryResult result = await _client.query(schema: getEventById, body: {
|
||||
'id': id,
|
||||
});
|
||||
if (result.hasException) {
|
||||
throw parseBackendError(result.exception);
|
||||
}
|
||||
|
||||
if (result.data == null || result.data!['client_event'] == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return EventModel.fromJson(result.data!['client_event']);
|
||||
}
|
||||
|
||||
Future<void> cancelClientEvent(String id) async {
|
||||
final QueryResult result = await _client.mutate(
|
||||
schema: cancelClientEventMutation,
|
||||
body: {'event_id': id},
|
||||
);
|
||||
if (result.hasException) {
|
||||
throw parseBackendError(result.exception);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> completeClientEvent(String id, {String? comment}) async {
|
||||
final QueryResult result = await _client.mutate(
|
||||
schema: completeClientEventMutation,
|
||||
body: {'event_id': id, if(comment!=null)'comment': comment.trim()},
|
||||
);
|
||||
if (result.hasException) {
|
||||
print(result.exception.toString());
|
||||
throw parseBackendError(result.exception);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> trackClientClockin(String positionStaffId) async {
|
||||
final QueryResult result = await _client.mutate(
|
||||
schema: trackClientClockinMutation,
|
||||
body: {'position_staff_id': positionStaffId},
|
||||
);
|
||||
if (result.hasException) {
|
||||
throw parseBackendError(result.exception);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> trackClientClockout(String positionStaffId) async {
|
||||
final QueryResult result = await _client.mutate(
|
||||
schema: trackClientClockoutMutation,
|
||||
body: {'position_staff_id': positionStaffId},
|
||||
);
|
||||
if (result.hasException) {
|
||||
throw parseBackendError(result.exception);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> notShowedPositionStaff(String positionStaffId) async {
|
||||
final QueryResult result = await _client.mutate(
|
||||
schema: notShowedPositionStaffMutation,
|
||||
body: {'position_staff_id': positionStaffId},
|
||||
);
|
||||
if (result.hasException) {
|
||||
throw parseBackendError(result.exception);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> replacePositionStaff(
|
||||
String positionStaffId, String reason) async {
|
||||
final QueryResult result = await _client.mutate(
|
||||
schema: replacePositionStaffMutation,
|
||||
body: {'position_staff_id': positionStaffId, 'reason': reason.trim()},
|
||||
);
|
||||
if (result.hasException) {
|
||||
throw parseBackendError(result.exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
272
mobile-apps/client-app/lib/features/events/data/events_gql.dart
Normal file
272
mobile-apps/client-app/lib/features/events/data/events_gql.dart
Normal file
@@ -0,0 +1,272 @@
|
||||
String getEventsQuery = '''
|
||||
query GetEvents (\$status: EventStatus!, \$first: Int!, \$after: String) {
|
||||
client_events(status: \$status, first: \$first, after: \$after) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
startCursor
|
||||
endCursor
|
||||
total
|
||||
count
|
||||
currentPage
|
||||
lastPage
|
||||
}
|
||||
edges {
|
||||
...node
|
||||
cursor
|
||||
}
|
||||
}
|
||||
}
|
||||
$nodeFragment
|
||||
''';
|
||||
|
||||
var getEventById = '''
|
||||
query GetEventById(\$id: ID!) {
|
||||
client_event(id: \$id) {
|
||||
$_eventFields
|
||||
}
|
||||
}
|
||||
|
||||
$fragments
|
||||
|
||||
''';
|
||||
|
||||
var nodeFragment = '''
|
||||
fragment node on EventEdge {
|
||||
node {
|
||||
$_eventsListFields
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String cancelClientEventMutation = r'''
|
||||
mutation cancel_client_event($event_id: ID!) {
|
||||
cancel_client_event(event_id: $event_id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String completeClientEventMutation = r'''
|
||||
mutation complete_client_event($event_id: ID!, $comment: String) {
|
||||
complete_client_event(event_id: $event_id, comment: $comment) {
|
||||
id
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String trackClientClockinMutation = r'''
|
||||
mutation track_client_clockin($position_staff_id: ID!) {
|
||||
track_client_clockin(position_staff_id: $position_staff_id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String trackClientClockoutMutation = r'''
|
||||
mutation track_client_clockout($position_staff_id: ID!) {
|
||||
track_client_clockout(position_staff_id: $position_staff_id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String notShowedPositionStaffMutation = r'''
|
||||
mutation not_showed_position_staff($position_staff_id: ID!) {
|
||||
not_showed_position_staff(position_staff_id: $position_staff_id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String replacePositionStaffMutation = r'''
|
||||
mutation replace_position_staff($position_staff_id: ID!, $reason: String!) {
|
||||
replace_position_staff(position_staff_id: $position_staff_id, reason: $reason) {
|
||||
id
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
String _eventsListFields = '''
|
||||
id
|
||||
business {
|
||||
id
|
||||
name
|
||||
registration
|
||||
avatar
|
||||
}
|
||||
hub {
|
||||
id
|
||||
name
|
||||
address
|
||||
}
|
||||
name
|
||||
status
|
||||
date
|
||||
start_time
|
||||
end_time
|
||||
purchase_order
|
||||
contract_type
|
||||
schedule_type
|
||||
''';
|
||||
|
||||
String _eventFields = '''
|
||||
id
|
||||
business {
|
||||
id
|
||||
name
|
||||
registration
|
||||
avatar
|
||||
...addons
|
||||
}
|
||||
hub {
|
||||
id
|
||||
name
|
||||
address
|
||||
}
|
||||
name
|
||||
status
|
||||
date
|
||||
start_time
|
||||
end_time
|
||||
purchase_order
|
||||
contract_type
|
||||
schedule_type
|
||||
additional_info
|
||||
addons {
|
||||
id
|
||||
name
|
||||
}
|
||||
tags {
|
||||
id
|
||||
name
|
||||
slug
|
||||
}
|
||||
...shifts
|
||||
|
||||
|
||||
''';
|
||||
|
||||
String fragments = '''fragment addons on Business {
|
||||
addons {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
fragment shifts on Event {
|
||||
shifts {
|
||||
id
|
||||
name
|
||||
address
|
||||
...full_address
|
||||
contacts {
|
||||
id
|
||||
first_name
|
||||
last_name
|
||||
title
|
||||
...auth_info
|
||||
}
|
||||
positions {
|
||||
id
|
||||
count
|
||||
start_time
|
||||
end_time
|
||||
rate
|
||||
break
|
||||
...business_skill
|
||||
...staff
|
||||
department {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment auth_info on BusinessMember {
|
||||
auth_info {
|
||||
email
|
||||
phone
|
||||
}
|
||||
}
|
||||
|
||||
fragment business_skill on EventShiftPosition {
|
||||
business_skill {
|
||||
id
|
||||
skill {
|
||||
id
|
||||
name
|
||||
slug
|
||||
price
|
||||
}
|
||||
price
|
||||
is_active
|
||||
}
|
||||
}
|
||||
|
||||
fragment full_address on EventShift {
|
||||
full_address {
|
||||
street_number
|
||||
zip_code
|
||||
latitude
|
||||
longitude
|
||||
formatted_address
|
||||
street
|
||||
region
|
||||
city
|
||||
country
|
||||
}
|
||||
}
|
||||
|
||||
fragment staff on EventShiftPosition {
|
||||
staff {
|
||||
id
|
||||
first_name
|
||||
last_name
|
||||
middle_name
|
||||
email
|
||||
phone
|
||||
avatar
|
||||
pivot {
|
||||
id
|
||||
status
|
||||
start_at
|
||||
end_at
|
||||
clock_in
|
||||
clock_out
|
||||
...staff_position
|
||||
...cancel_reason
|
||||
rating {
|
||||
id
|
||||
rating
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment cancel_reason on EventShiftPositionStaff {
|
||||
cancel_reason {
|
||||
type
|
||||
reason
|
||||
details
|
||||
}
|
||||
}
|
||||
|
||||
fragment staff_position on EventShiftPositionStaff {
|
||||
position {
|
||||
id
|
||||
count
|
||||
start_time
|
||||
end_time
|
||||
rate
|
||||
break
|
||||
business_skill {
|
||||
id
|
||||
price
|
||||
skill{
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}''';
|
||||
@@ -0,0 +1,137 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:krow/core/entity/event_entity.dart';
|
||||
import 'package:krow/features/events/data/events_api_provider.dart';
|
||||
import 'package:krow/features/events/domain/events_repository.dart';
|
||||
|
||||
@Singleton(as: EventsRepository)
|
||||
class EventsRepositoryImpl extends EventsRepository {
|
||||
final EventsApiProvider _apiProvider;
|
||||
StreamController<EventStatus>? _statusController;
|
||||
|
||||
EventsRepositoryImpl({required EventsApiProvider apiProvider})
|
||||
: _apiProvider = apiProvider;
|
||||
|
||||
@override
|
||||
Stream<EventStatus> get statusStream {
|
||||
_statusController ??= StreamController<EventStatus>.broadcast();
|
||||
return _statusController!.stream;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<EventEntity>> getEvents(
|
||||
{String? lastItemId, required EventStatus statusFilter}) async {
|
||||
try {
|
||||
var paginationWrapper = await _apiProvider.fetchEvents(
|
||||
statusFilterToGqlString(statusFilter),
|
||||
after: lastItemId);
|
||||
return paginationWrapper?.edges
|
||||
.map((e) => EventEntity.fromEventDto(
|
||||
e.node,
|
||||
cursor: paginationWrapper.pageInfo.hasNextPage
|
||||
? e.cursor
|
||||
: null,
|
||||
))
|
||||
.toList() ??
|
||||
[];
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_statusController?.close();
|
||||
}
|
||||
|
||||
statusFilterToGqlString(EventStatus statusFilter) {
|
||||
return statusFilter.name;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> cancelClientEvent(String id, EventStatus? status) async {
|
||||
try {
|
||||
await _apiProvider.cancelClientEvent(id);
|
||||
if (!(_statusController?.isClosed ?? true)) {
|
||||
if (status != null) _statusController?.add(status);
|
||||
}
|
||||
} catch (exception) {
|
||||
debugPrint(exception.toString());
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> completeClientEvent(String id, {String? comment}) async {
|
||||
try {
|
||||
await _apiProvider.completeClientEvent(id, comment: comment);
|
||||
if (!(_statusController?.isClosed ?? true)) {
|
||||
_statusController?.add(EventStatus.finished);
|
||||
_statusController?.add(EventStatus.completed);
|
||||
}
|
||||
} catch (exception) {
|
||||
debugPrint(exception.toString());
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> trackClientClockin(String positionStaffId) async {
|
||||
try {
|
||||
await _apiProvider.trackClientClockin(positionStaffId);
|
||||
} catch (exception) {
|
||||
debugPrint(exception.toString());
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> trackClientClockout(String positionStaffId) async {
|
||||
try {
|
||||
await _apiProvider.trackClientClockout(positionStaffId);
|
||||
} catch (exception) {
|
||||
debugPrint(exception.toString());
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> notShowedPositionStaff(String positionStaffId) async {
|
||||
try {
|
||||
await _apiProvider.notShowedPositionStaff(positionStaffId);
|
||||
} catch (exception) {
|
||||
debugPrint(exception.toString());
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> replacePositionStaff(
|
||||
String positionStaffId, String reason) async {
|
||||
try {
|
||||
await _apiProvider.replacePositionStaff(positionStaffId, reason);
|
||||
} catch (exception) {
|
||||
debugPrint(exception.toString());
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void refreshEvents(EventStatus status) {
|
||||
if (!(_statusController?.isClosed ?? true)) {
|
||||
_statusController?.add(status);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<EventEntity?> getEventById(String id) async {
|
||||
return await _apiProvider.fetchEventById(id).then((event) {
|
||||
if (event != null) {
|
||||
return EventEntity.fromEventDto(event);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:krow/core/application/clients/api/api_exception.dart';
|
||||
import 'package:krow/core/application/di/injectable.dart';
|
||||
import 'package:krow/core/data/models/staff/pivot.dart';
|
||||
import 'package:krow/core/entity/event_entity.dart';
|
||||
import 'package:krow/core/entity/position_entity.dart';
|
||||
import 'package:krow/core/entity/shift_entity.dart';
|
||||
import 'package:krow/core/entity/staff_contact_entity.dart';
|
||||
import 'package:krow/core/sevices/create_event_service/create_event_service.dart';
|
||||
import 'package:krow/core/sevices/geofencin_service.dart';
|
||||
import 'package:krow/features/events/domain/events_repository.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
part 'event_details_event.dart';
|
||||
part 'event_details_state.dart';
|
||||
|
||||
class EventDetailsBloc extends Bloc<EventDetailsEvent, EventDetailsState> {
|
||||
final EventsRepository _repository;
|
||||
Timer? _timer;
|
||||
var disabledPolling = false;
|
||||
|
||||
EventDetailsBloc(EventEntity event, this._repository, isPreview)
|
||||
: super(EventDetailsState(event: event, isPreview: isPreview)) {
|
||||
|
||||
on<EventDetailsInitialEvent>(_onInit);
|
||||
on<EventDetailsUpdateEvent>(_onUpdateEntity);
|
||||
on<OnShiftHeaderTapEvent>(_onShiftHeaderTap);
|
||||
on<OnRoleHeaderTapEvent>(_onRoleHeaderTap);
|
||||
on<OnAssignedStaffHeaderTapEvent>(_onAssignedStaffHeaderTap);
|
||||
on<CompleteEventEvent>(_onCompleteEvent);
|
||||
on<CancelClientEvent>(_onCancelClientEvent);
|
||||
on<TrackClientClockin>(_onTrackClientClockin);
|
||||
on<TrackClientClockout>(_onTrackClientClockout);
|
||||
on<NotShowedPositionStaffEvent>(_onNotShowedPositionStaff);
|
||||
on<ReplacePositionStaffEvent>(_onReplacePositionStaff);
|
||||
on<CreateEventPostEvent>(_createEvent);
|
||||
on<DetailsDeleteDraftEvent>(_onDetailsDeleteDraftEvent);
|
||||
on<DetailsPublishEvent>(_onDetailsPublishEvent);
|
||||
on<GeofencingEvent>(_onGeofencingEvent);
|
||||
on<RefreshEventDetailsEvent>(_onRefreshEventDetailsEvent);
|
||||
on<DisablePollingEvent>(_onDisablePollingEvent);
|
||||
on<EnablePollingEvent>(_onEnablePollingEvent);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_timer?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
FutureOr<void> _onShiftHeaderTap(OnShiftHeaderTapEvent event, emit) {
|
||||
emit(state.copyWith(
|
||||
shifts: state.shifts
|
||||
.map((e) =>
|
||||
e.copyWith(isExpanded: e == event.shiftState && !e.isExpanded))
|
||||
.toList()));
|
||||
}
|
||||
|
||||
FutureOr<void> _onRoleHeaderTap(OnRoleHeaderTapEvent event, emit) {
|
||||
emit(state.copyWith(
|
||||
shifts: state.shifts
|
||||
.map((e) => e.copyWith(
|
||||
positions: e.positions
|
||||
.map((r) => r.copyWith(
|
||||
isExpanded:
|
||||
r.position.id == event.roleState.position.id &&
|
||||
!r.isExpanded))
|
||||
.toList()))
|
||||
.toList()));
|
||||
}
|
||||
|
||||
FutureOr<void> _onAssignedStaffHeaderTap(
|
||||
OnAssignedStaffHeaderTapEvent event, emit) {
|
||||
emit(state.copyWith(
|
||||
shifts: state.shifts
|
||||
.map((e) => e.copyWith(
|
||||
positions: e.positions
|
||||
.map((r) => r.copyWith(
|
||||
isStaffExpanded:
|
||||
r.position.id == event.roleState.position.id &&
|
||||
!r.isStaffExpanded))
|
||||
.toList()))
|
||||
.toList()));
|
||||
}
|
||||
|
||||
FutureOr<void> _onCompleteEvent(CompleteEventEvent event, emit) async {
|
||||
try {
|
||||
emit(state.copyWith(inLoading: true));
|
||||
await _repository.completeClientEvent(state.event.id,
|
||||
comment: event.comment);
|
||||
emit(state.copyWith(
|
||||
event: state.event.copyWith(status: EventStatus.completed)));
|
||||
} catch (e) {
|
||||
if (e is DisplayableException) {
|
||||
emit(state.copyWith(showErrorPopup: e.message));
|
||||
return;
|
||||
}
|
||||
}
|
||||
emit(state.copyWith(inLoading: false));
|
||||
|
||||
}
|
||||
|
||||
FutureOr<void> _onCancelClientEvent(CancelClientEvent event, emit) async {
|
||||
emit(state.copyWith(inLoading: true));
|
||||
try {
|
||||
await _repository.cancelClientEvent(state.event.id, state.event.status);
|
||||
emit(state.copyWith(
|
||||
event: state.event.copyWith(status: EventStatus.canceled)));
|
||||
} catch (e) {
|
||||
if (e is DisplayableException) {
|
||||
emit(state.copyWith(showErrorPopup: e.message));
|
||||
}
|
||||
}
|
||||
emit(state.copyWith(inLoading: false));
|
||||
}
|
||||
|
||||
FutureOr<void> _onInit(EventDetailsInitialEvent event, emit) {
|
||||
getIt<CreateEventService>().eventsRepository = _repository;
|
||||
emit(state.copyWith(
|
||||
inLoading: true,
|
||||
shifts: state.event.shifts
|
||||
?.map((shift) => ShiftState(
|
||||
shift: shift,
|
||||
positions: shift.positions
|
||||
.map((position) => PositionState(position: position))
|
||||
.toList()))
|
||||
.toList() ??
|
||||
[],
|
||||
));
|
||||
|
||||
add(RefreshEventDetailsEvent());
|
||||
|
||||
|
||||
|
||||
if (!state.isPreview) {
|
||||
startPoling();
|
||||
}else{
|
||||
emit(state.copyWith(inLoading: false));
|
||||
}
|
||||
}
|
||||
|
||||
FutureOr<void> _onTrackClientClockin(TrackClientClockin event, emit) async {
|
||||
emit(state.copyWith(inLoading: true));
|
||||
if (!(await checkGeofancing(event.staffContact))) {
|
||||
emit(state.copyWith(inLoading: false));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await _repository.trackClientClockin(event.staffContact.id);
|
||||
event.staffContact.status = PivotStatus.ongoing;
|
||||
} catch (e) {
|
||||
if (e is DisplayableException) {
|
||||
emit(state.copyWith(showErrorPopup: e.message));
|
||||
}
|
||||
}
|
||||
emit(state.copyWith(inLoading: false));
|
||||
}
|
||||
|
||||
FutureOr<void> _onTrackClientClockout(TrackClientClockout event, emit) async {
|
||||
emit(state.copyWith(inLoading: true));
|
||||
try {
|
||||
await _repository.trackClientClockout(event.staffContact.id);
|
||||
event.staffContact.status = PivotStatus.confirmed;
|
||||
} catch (e) {
|
||||
if (e is DisplayableException) {
|
||||
emit(state.copyWith(showErrorPopup: e.message));
|
||||
}
|
||||
}
|
||||
emit(state.copyWith(inLoading: false));
|
||||
}
|
||||
|
||||
FutureOr<void> _onNotShowedPositionStaff(
|
||||
NotShowedPositionStaffEvent event, emit) async {
|
||||
emit(state.copyWith(inLoading: true));
|
||||
try {
|
||||
await _repository.notShowedPositionStaff(event.positionStaffId);
|
||||
} catch (e) {
|
||||
if (e is DisplayableException) {
|
||||
emit(state.copyWith(showErrorPopup: e.message));
|
||||
}
|
||||
}
|
||||
emit(state.copyWith(inLoading: false));
|
||||
}
|
||||
|
||||
FutureOr<void> _onReplacePositionStaff(
|
||||
ReplacePositionStaffEvent event, emit) async {
|
||||
emit(state.copyWith(inLoading: true));
|
||||
try {
|
||||
await _repository.replacePositionStaff(
|
||||
event.positionStaffId, event.reason);
|
||||
} catch (e) {
|
||||
if (e is DisplayableException) {
|
||||
emit(state.copyWith(showErrorPopup: e.message));
|
||||
}else{
|
||||
emit(state.copyWith(
|
||||
showErrorPopup: 'Something went wrong'));
|
||||
}
|
||||
}
|
||||
emit(state.copyWith(inLoading: false));
|
||||
}
|
||||
|
||||
void _createEvent(
|
||||
CreateEventPostEvent event, Emitter<EventDetailsState> emit) async {
|
||||
emit(state.copyWith(inLoading: true));
|
||||
try {
|
||||
if (state.event.id.isEmpty) {
|
||||
await getIt<CreateEventService>().createEventService(
|
||||
state.event,
|
||||
);
|
||||
} else {
|
||||
await getIt<CreateEventService>().updateEvent(
|
||||
state.event,
|
||||
);
|
||||
}
|
||||
// emit(state.copyWith(inLoading: false, ));
|
||||
emit(state.copyWith(inLoading: false, needDeepPop: true));
|
||||
} catch (e) {
|
||||
if (e is DisplayableException) {
|
||||
emit(state.copyWith(inLoading: false, showErrorPopup: e.message));
|
||||
return;
|
||||
} else {
|
||||
print(e);
|
||||
emit(state.copyWith(
|
||||
inLoading: false, showErrorPopup: 'Something went wrong'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
emit(state.copyWith(inLoading: false));
|
||||
}
|
||||
|
||||
void _onDetailsDeleteDraftEvent(DetailsDeleteDraftEvent event, emit) async {
|
||||
emit(state.copyWith(inLoading: true));
|
||||
try {
|
||||
await getIt<CreateEventService>().deleteDraft(
|
||||
state.event,
|
||||
);
|
||||
emit(state.copyWith(inLoading: false, needDeepPop: true));
|
||||
} catch (e) {
|
||||
if (e is DisplayableException) {
|
||||
emit(state.copyWith(inLoading: false, showErrorPopup: e.message));
|
||||
return;
|
||||
} else {
|
||||
emit(state.copyWith(
|
||||
inLoading: false, showErrorPopup: 'Something went wrong'));
|
||||
}
|
||||
}
|
||||
emit(state.copyWith(inLoading: false));
|
||||
|
||||
}
|
||||
|
||||
void _onDetailsPublishEvent(DetailsPublishEvent event, emit) async {
|
||||
emit(state.copyWith(inLoading: true));
|
||||
try {
|
||||
await getIt<CreateEventService>().publishEvent(
|
||||
state.event,
|
||||
);
|
||||
emit(state.copyWith(inLoading: false, needDeepPop: true));
|
||||
} catch (e) {
|
||||
if (e is DisplayableException) {
|
||||
emit(state.copyWith(inLoading: false, showErrorPopup: e.message));
|
||||
return;
|
||||
} else {
|
||||
print(e);
|
||||
emit(state.copyWith(
|
||||
inLoading: false, showErrorPopup: 'Something went wrong'));
|
||||
}
|
||||
}
|
||||
emit(state.copyWith(inLoading: false));
|
||||
|
||||
}
|
||||
|
||||
void startPoling() {
|
||||
_timer = Timer.periodic(const Duration(seconds: 5), (timer) async {
|
||||
if(state.isPreview) return;
|
||||
add(RefreshEventDetailsEvent());
|
||||
});
|
||||
}
|
||||
|
||||
void _onRefreshEventDetailsEvent(
|
||||
RefreshEventDetailsEvent event, Emitter<EventDetailsState> emit) async {
|
||||
if(state.isPreview) return;
|
||||
try {
|
||||
await _repository.getEventById(state.event.id).then((event) {
|
||||
if (event != null) {
|
||||
add(EventDetailsUpdateEvent(event));
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
|
||||
void _onUpdateEntity(EventDetailsUpdateEvent event, emit) {
|
||||
if(disabledPolling) return;
|
||||
emit(
|
||||
state.copyWith(
|
||||
inLoading: false,
|
||||
event: event.event,
|
||||
shifts: event.event.shifts?.map(
|
||||
(shift) {
|
||||
var oldShift = state.shifts
|
||||
.firstWhereOrNull((e) => e.shift.id == shift.id);
|
||||
return ShiftState(
|
||||
isExpanded: oldShift?.isExpanded ?? false,
|
||||
shift: shift,
|
||||
positions: shift.positions.map(
|
||||
(position) {
|
||||
var oldPosition = oldShift?.positions.firstWhereOrNull(
|
||||
(e) => e.position.id == position.id);
|
||||
return PositionState(
|
||||
position: position,
|
||||
isExpanded: oldPosition?.isExpanded ?? false,
|
||||
isStaffExpanded:
|
||||
oldPosition?.isStaffExpanded ?? false);
|
||||
},
|
||||
).toList());
|
||||
},
|
||||
).toList() ??
|
||||
[],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> checkGeofancing(StaffContact staffContact) async {
|
||||
var permissionResult =
|
||||
await GeofencingService().requestGeolocationPermission();
|
||||
|
||||
if (permissionResult == GeolocationStatus.enabled) {
|
||||
var result = await GeofencingService().isInRangeCheck(
|
||||
pointLatitude:
|
||||
staffContact.parentPosition?.parentShift?.fullAddress?.latitude ??
|
||||
0,
|
||||
pointLongitude:
|
||||
staffContact.parentPosition?.parentShift?.fullAddress?.longitude ??
|
||||
0,
|
||||
);
|
||||
if (result) {
|
||||
return true;
|
||||
} else {
|
||||
add(GeofencingEvent(GeofencingDialogState.tooFar));
|
||||
}
|
||||
} else if (permissionResult == GeolocationStatus.disabled) {
|
||||
add(GeofencingEvent(GeofencingDialogState.locationDisabled));
|
||||
} else if (permissionResult == GeolocationStatus.prohibited) {
|
||||
add(GeofencingEvent(GeofencingDialogState.goToSettings));
|
||||
} else if (permissionResult == GeolocationStatus.denied) {
|
||||
add(GeofencingEvent(GeofencingDialogState.permissionDenied));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _onGeofencingEvent(GeofencingEvent event, emit) {
|
||||
emit(state.copyWith(geofencingDialogState: event.dialogState));
|
||||
if (event.dialogState != GeofencingDialogState.none) {
|
||||
add(GeofencingEvent(GeofencingDialogState.none));
|
||||
}
|
||||
}
|
||||
|
||||
void _onDisablePollingEvent(DisablePollingEvent event, emit) {
|
||||
disabledPolling = true;
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
}
|
||||
|
||||
void _onEnablePollingEvent(EnablePollingEvent event, emit) {
|
||||
disabledPolling = false;
|
||||
|
||||
if (_timer == null) {
|
||||
startPoling();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onEvent(EventDetailsEvent event) {
|
||||
print(event);
|
||||
super.onEvent(event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
part of 'event_details_bloc.dart';
|
||||
|
||||
@immutable
|
||||
abstract class EventDetailsEvent {}
|
||||
|
||||
class EventDetailsInitialEvent extends EventDetailsEvent {
|
||||
|
||||
EventDetailsInitialEvent();
|
||||
}
|
||||
|
||||
class EventDetailsUpdateEvent extends EventDetailsEvent {
|
||||
final EventEntity event;
|
||||
|
||||
EventDetailsUpdateEvent(this.event);
|
||||
}
|
||||
|
||||
class OnShiftHeaderTapEvent extends EventDetailsEvent {
|
||||
final ShiftState shiftState;
|
||||
|
||||
OnShiftHeaderTapEvent(this.shiftState);
|
||||
}
|
||||
|
||||
class OnRoleHeaderTapEvent extends EventDetailsEvent {
|
||||
final PositionState roleState;
|
||||
|
||||
OnRoleHeaderTapEvent(this.roleState);
|
||||
}
|
||||
|
||||
class OnAssignedStaffHeaderTapEvent extends EventDetailsEvent {
|
||||
final PositionState roleState;
|
||||
|
||||
OnAssignedStaffHeaderTapEvent(this.roleState);
|
||||
}
|
||||
|
||||
class CompleteEventEvent extends EventDetailsEvent {
|
||||
final String? comment;
|
||||
|
||||
CompleteEventEvent({this.comment});
|
||||
}
|
||||
|
||||
class CancelClientEvent extends EventDetailsEvent {
|
||||
CancelClientEvent();
|
||||
}
|
||||
|
||||
class CompleteClientEvent extends EventDetailsEvent {
|
||||
final String id;
|
||||
final String? comment;
|
||||
|
||||
CompleteClientEvent(this.id, {this.comment});
|
||||
}
|
||||
|
||||
class TrackClientClockin extends EventDetailsEvent {
|
||||
final StaffContact staffContact;
|
||||
|
||||
TrackClientClockin(this.staffContact);
|
||||
}
|
||||
|
||||
class TrackClientClockout extends EventDetailsEvent {
|
||||
final StaffContact staffContact;
|
||||
|
||||
TrackClientClockout(this.staffContact);
|
||||
}
|
||||
|
||||
class NotShowedPositionStaffEvent extends EventDetailsEvent {
|
||||
final String positionStaffId;
|
||||
|
||||
NotShowedPositionStaffEvent(this.positionStaffId);
|
||||
}
|
||||
|
||||
class ReplacePositionStaffEvent extends EventDetailsEvent {
|
||||
final String positionStaffId;
|
||||
final String reason;
|
||||
|
||||
ReplacePositionStaffEvent(this.positionStaffId, this.reason);
|
||||
}
|
||||
|
||||
class CreateEventPostEvent extends EventDetailsEvent {
|
||||
CreateEventPostEvent();
|
||||
}
|
||||
|
||||
class DetailsDeleteDraftEvent extends EventDetailsEvent {
|
||||
DetailsDeleteDraftEvent();
|
||||
}
|
||||
|
||||
class DetailsPublishEvent extends EventDetailsEvent {
|
||||
DetailsPublishEvent();
|
||||
}
|
||||
|
||||
class GeofencingEvent extends EventDetailsEvent {
|
||||
final GeofencingDialogState dialogState;
|
||||
|
||||
GeofencingEvent(this.dialogState);
|
||||
}
|
||||
|
||||
class RefreshEventDetailsEvent extends EventDetailsEvent {
|
||||
RefreshEventDetailsEvent();
|
||||
}
|
||||
|
||||
class DisablePollingEvent extends EventDetailsEvent {
|
||||
DisablePollingEvent();
|
||||
}
|
||||
|
||||
class EnablePollingEvent extends EventDetailsEvent {
|
||||
EnablePollingEvent();
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
part of 'event_details_bloc.dart';
|
||||
|
||||
enum GeofencingDialogState {
|
||||
none,
|
||||
tooFar,
|
||||
locationDisabled,
|
||||
goToSettings,
|
||||
permissionDenied,
|
||||
}
|
||||
|
||||
@immutable
|
||||
class EventDetailsState {
|
||||
final EventEntity event;
|
||||
final List<ShiftState> shifts;
|
||||
final bool inLoading;
|
||||
final bool isPreview;
|
||||
final String? showErrorPopup;
|
||||
final bool needDeepPop;
|
||||
final GeofencingDialogState geofencingDialogState;
|
||||
|
||||
const EventDetailsState({
|
||||
required this.event,
|
||||
this.shifts = const [],
|
||||
this.inLoading = false,
|
||||
this.isPreview = false,
|
||||
this.needDeepPop = false,
|
||||
this.showErrorPopup,
|
||||
this.geofencingDialogState = GeofencingDialogState.none,
|
||||
});
|
||||
|
||||
EventDetailsState copyWith({
|
||||
EventEntity? event,
|
||||
List<ShiftState>? shifts,
|
||||
bool? inLoading,
|
||||
bool? isPreview,
|
||||
bool? needDeepPop,
|
||||
String? showErrorPopup,
|
||||
GeofencingDialogState? geofencingDialogState,
|
||||
}) {
|
||||
return EventDetailsState(
|
||||
event: event ?? this.event,
|
||||
shifts: shifts ?? this.shifts,
|
||||
inLoading: inLoading ?? this.inLoading,
|
||||
isPreview: isPreview ?? this.isPreview,
|
||||
needDeepPop: needDeepPop ?? this.needDeepPop,
|
||||
showErrorPopup: showErrorPopup,
|
||||
geofencingDialogState:
|
||||
geofencingDialogState ?? GeofencingDialogState.none,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ShiftState {
|
||||
final bool isExpanded;
|
||||
final ShiftEntity shift;
|
||||
final List<PositionState> positions;
|
||||
|
||||
ShiftState({
|
||||
this.isExpanded = false,
|
||||
required this.shift,
|
||||
this.positions = const [],
|
||||
});
|
||||
|
||||
ShiftState copyWith({
|
||||
bool? isExpanded,
|
||||
ShiftEntity? shift,
|
||||
List<PositionState>? positions,
|
||||
DateTime? date,
|
||||
}) {
|
||||
return ShiftState(
|
||||
isExpanded: isExpanded ?? this.isExpanded,
|
||||
shift: shift ?? this.shift,
|
||||
positions: positions ?? this.positions,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PositionState {
|
||||
final PositionEntity position;
|
||||
final bool isExpanded;
|
||||
final bool isStaffExpanded;
|
||||
|
||||
PositionState(
|
||||
{required this.position,
|
||||
this.isExpanded = false,
|
||||
this.isStaffExpanded = false});
|
||||
|
||||
PositionState copyWith({
|
||||
PositionEntity? position,
|
||||
bool? isExpanded,
|
||||
bool? isStaffExpanded,
|
||||
}) {
|
||||
return PositionState(
|
||||
position: position ?? this.position,
|
||||
isExpanded: isExpanded ?? this.isExpanded,
|
||||
isStaffExpanded: isStaffExpanded ?? this.isStaffExpanded,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/core/application/di/injectable.dart';
|
||||
import 'package:krow/core/entity/event_entity.dart';
|
||||
import 'package:krow/features/events/domain/blocs/events_list_bloc/events_event.dart';
|
||||
import 'package:krow/features/events/domain/blocs/events_list_bloc/events_state.dart';
|
||||
import 'package:krow/features/events/domain/events_repository.dart';
|
||||
|
||||
class EventsBloc extends Bloc<EventsEvent, EventsState> {
|
||||
EventsBloc()
|
||||
: super(const EventsState(tabs: {
|
||||
0: {
|
||||
0: EventTabState(
|
||||
items: [],
|
||||
inLoading: false,
|
||||
hasMoreItems: true,
|
||||
status: EventStatus.pending),
|
||||
1: EventTabState(
|
||||
items: [],
|
||||
inLoading: false,
|
||||
hasMoreItems: true,
|
||||
status: EventStatus.assigned),
|
||||
2: EventTabState(
|
||||
items: [],
|
||||
inLoading: false,
|
||||
hasMoreItems: true,
|
||||
status: EventStatus.confirmed),
|
||||
},
|
||||
1: {
|
||||
0: EventTabState(
|
||||
items: [],
|
||||
inLoading: false,
|
||||
hasMoreItems: true,
|
||||
status: EventStatus.active),
|
||||
1: EventTabState(
|
||||
items: [],
|
||||
inLoading: false,
|
||||
hasMoreItems: true,
|
||||
status: EventStatus.finished),
|
||||
},
|
||||
2: {
|
||||
0: EventTabState(
|
||||
items: [],
|
||||
inLoading: false,
|
||||
hasMoreItems: true,
|
||||
status: EventStatus.completed),
|
||||
1: EventTabState(
|
||||
items: [],
|
||||
inLoading: false,
|
||||
hasMoreItems: true,
|
||||
status: EventStatus.closed),
|
||||
2: EventTabState(
|
||||
items: [],
|
||||
inLoading: false,
|
||||
hasMoreItems: true,
|
||||
status: EventStatus.canceled),
|
||||
},
|
||||
3: {
|
||||
0: EventTabState(
|
||||
items: [],
|
||||
inLoading: false,
|
||||
hasMoreItems: true,
|
||||
status: EventStatus.draft),
|
||||
},
|
||||
})) {
|
||||
on<EventsInitialEvent>(_onInitial);
|
||||
on<EventsTabChangedEvent>(_onTabChanged);
|
||||
on<EventsSubTabChangedEvent>(_onSubTabChanged);
|
||||
on<LoadTabEventEvent>(_onLoadTabItems);
|
||||
on<LoadMoreEventEvent>(_onLoadMoreTabItems);
|
||||
|
||||
getIt<EventsRepository>().statusStream.listen((event) {
|
||||
(int, int)? pair = findTabIndexForStatus(state.tabs, event);
|
||||
if (pair != null) {
|
||||
add(LoadTabEventEvent(tabIndex: pair.$1, subTabIndex: pair.$2));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _onInitial(EventsInitialEvent event, emit) async {
|
||||
add(const LoadTabEventEvent(tabIndex: 0, subTabIndex: 0));
|
||||
}
|
||||
|
||||
Future<void> _onTabChanged(EventsTabChangedEvent event, emit) async {
|
||||
emit(state.copyWith(tabIndex: event.tabIndex, subTabIndex: 0));
|
||||
final currentTabState = state.tabs[event.tabIndex]![0]!;
|
||||
if (currentTabState.items.isEmpty && !currentTabState.inLoading) {
|
||||
add(LoadTabEventEvent(tabIndex: event.tabIndex, subTabIndex: 0));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onSubTabChanged(EventsSubTabChangedEvent event, emit) async {
|
||||
emit(state.copyWith(subTabIndex: event.subTabIndex));
|
||||
final currentTabState = state.tabs[state.tabIndex]![event.subTabIndex]!;
|
||||
if (currentTabState.items.isEmpty && !currentTabState.inLoading) {
|
||||
add(LoadTabEventEvent(
|
||||
tabIndex: state.tabIndex, subTabIndex: event.subTabIndex));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onLoadTabItems(LoadTabEventEvent event, emit) async {
|
||||
await _fetchEvents(event.tabIndex, event.subTabIndex, null, emit);
|
||||
}
|
||||
|
||||
Future<void> _onLoadMoreTabItems(LoadMoreEventEvent event, emit) async {
|
||||
final currentTabState = state.tabs[event.tabIndex]![event.subTabIndex]!;
|
||||
if (!currentTabState.hasMoreItems || currentTabState.inLoading) return;
|
||||
await _fetchEvents(
|
||||
event.tabIndex, event.subTabIndex, currentTabState.items, emit);
|
||||
}
|
||||
|
||||
_fetchEvents(int tabIndex, int subTabIndex, List<EventEntity>? previousItems,
|
||||
emit) async {
|
||||
if (previousItems != null && previousItems.lastOrNull?.cursor == null) {
|
||||
return;
|
||||
}
|
||||
final currentTabState = state.tabs[tabIndex]![subTabIndex]!;
|
||||
var newState = state.copyWith(
|
||||
tabs: {
|
||||
...state.tabs,
|
||||
tabIndex: {
|
||||
...state.tabs[tabIndex]!,
|
||||
subTabIndex: currentTabState.copyWith(inLoading: true)
|
||||
},
|
||||
},
|
||||
);
|
||||
emit(newState);
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
try {
|
||||
var items = await getIt<EventsRepository>().getEvents(
|
||||
statusFilter: currentTabState.status,
|
||||
lastItemId: previousItems?.lastOrNull?.cursor,
|
||||
);
|
||||
|
||||
var newState = state.copyWith(
|
||||
tabs: {
|
||||
...state.tabs,
|
||||
tabIndex: {
|
||||
...state.tabs[tabIndex]!,
|
||||
subTabIndex: currentTabState.copyWith(
|
||||
items: (previousItems ?? [])..addAll(items),
|
||||
hasMoreItems: items.isNotEmpty,
|
||||
inLoading: false,
|
||||
)
|
||||
},
|
||||
},
|
||||
);
|
||||
emit(newState);
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
emit(state.copyWith(errorMessage: e.toString()));
|
||||
emit(state.copyWith(
|
||||
tabs: {
|
||||
...state.tabs,
|
||||
tabIndex: {
|
||||
...state.tabs[tabIndex]!,
|
||||
subTabIndex: currentTabState.copyWith(
|
||||
items: (previousItems ?? []),
|
||||
hasMoreItems: false,
|
||||
inLoading: false,
|
||||
)
|
||||
},
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
getIt<EventsRepository>().dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
(int, int)? findTabIndexForStatus(
|
||||
Map<int, Map<int, EventTabState>> tabs, EventStatus status) {
|
||||
for (var tabEntry in tabs.entries) {
|
||||
for (var subTabEntry in tabEntry.value.entries) {
|
||||
if (subTabEntry.value.status == status) {
|
||||
return (tabEntry.key, subTabEntry.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
sealed class EventsEvent {
|
||||
const EventsEvent();
|
||||
}
|
||||
|
||||
class EventsInitialEvent extends EventsEvent {
|
||||
const EventsInitialEvent();
|
||||
}
|
||||
|
||||
class EventsTabChangedEvent extends EventsEvent {
|
||||
final int tabIndex;
|
||||
|
||||
const EventsTabChangedEvent({required this.tabIndex});
|
||||
}
|
||||
|
||||
class EventsSubTabChangedEvent extends EventsEvent {
|
||||
final int subTabIndex;
|
||||
|
||||
const EventsSubTabChangedEvent({required this.subTabIndex});
|
||||
}
|
||||
|
||||
class LoadTabEventEvent extends EventsEvent {
|
||||
final int tabIndex;
|
||||
final int subTabIndex;
|
||||
|
||||
const LoadTabEventEvent({required this.tabIndex, required this.subTabIndex});
|
||||
}
|
||||
|
||||
class LoadMoreEventEvent extends EventsEvent {
|
||||
final int tabIndex;
|
||||
final int subTabIndex;
|
||||
|
||||
const LoadMoreEventEvent({required this.tabIndex, required this.subTabIndex});
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import 'package:krow/core/entity/event_entity.dart';
|
||||
|
||||
class EventsState {
|
||||
final bool inLoading;
|
||||
final int tabIndex;
|
||||
final int subTabIndex;
|
||||
final String? errorMessage;
|
||||
|
||||
final Map<int, Map<int, EventTabState>> tabs;
|
||||
|
||||
const EventsState(
|
||||
{this.inLoading = false,
|
||||
this.tabIndex = 0,
|
||||
this.subTabIndex = 0,
|
||||
this.errorMessage,
|
||||
required this.tabs});
|
||||
|
||||
EventsState copyWith({
|
||||
bool? inLoading,
|
||||
int? tabIndex,
|
||||
int? subTabIndex,
|
||||
String? errorMessage,
|
||||
Map<int, Map<int, EventTabState>>? tabs,
|
||||
}) {
|
||||
return EventsState(
|
||||
inLoading: inLoading ?? this.inLoading,
|
||||
tabIndex: tabIndex ?? this.tabIndex,
|
||||
subTabIndex: subTabIndex ?? this.subTabIndex,
|
||||
tabs: tabs ?? this.tabs,
|
||||
errorMessage: errorMessage,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EventTabState {
|
||||
final List<EventEntity> items;
|
||||
final bool inLoading;
|
||||
final bool hasMoreItems;
|
||||
|
||||
final EventStatus status;
|
||||
|
||||
const EventTabState({
|
||||
required this.items,
|
||||
this.inLoading = false,
|
||||
this.hasMoreItems = true,
|
||||
required this.status,
|
||||
});
|
||||
|
||||
EventTabState copyWith({
|
||||
List<EventEntity>? items,
|
||||
bool? inLoading,
|
||||
bool? hasMoreItems,
|
||||
}) {
|
||||
return EventTabState(
|
||||
items: items ?? this.items,
|
||||
inLoading: inLoading ?? this.inLoading,
|
||||
hasMoreItems: hasMoreItems ?? this.hasMoreItems,
|
||||
status: status,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'package:krow/core/entity/event_entity.dart';
|
||||
|
||||
abstract class EventsRepository {
|
||||
Stream<EventStatus> get statusStream;
|
||||
|
||||
Future<List<EventEntity>> getEvents(
|
||||
{String? lastItemId, required EventStatus statusFilter});
|
||||
|
||||
Future<EventEntity?> getEventById(String id);
|
||||
|
||||
void dispose();
|
||||
|
||||
Future<void> cancelClientEvent(String id, EventStatus? status);
|
||||
|
||||
Future<void> completeClientEvent(String id, {String? comment});
|
||||
|
||||
Future<void> trackClientClockin(String positionStaffId);
|
||||
|
||||
Future<void> trackClientClockout(String positionStaffId);
|
||||
|
||||
Future<void> notShowedPositionStaff(String positionStaffId);
|
||||
|
||||
Future<void> replacePositionStaff(String positionStaffId, String reason);
|
||||
|
||||
void refreshEvents(EventStatus status);
|
||||
}
|
||||
12
mobile-apps/client-app/lib/features/events/events_flow.dart
Normal file
12
mobile-apps/client-app/lib/features/events/events_flow.dart
Normal file
@@ -0,0 +1,12 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@RoutePage()
|
||||
class EventsFlowScreen extends StatelessWidget {
|
||||
const EventsFlowScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const AutoRouter();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/core/application/di/injectable.dart';
|
||||
import 'package:krow/core/entity/event_entity.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/scroll_layout_helper.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_app_bar.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_popup_button.dart';
|
||||
import 'package:krow/features/events/domain/blocs/details/event_details_bloc.dart';
|
||||
import 'package:krow/features/events/domain/events_repository.dart';
|
||||
import 'package:krow/features/events/presentation/event_details/widgets/event_completed_by_card_widget.dart';
|
||||
import 'package:krow/features/events/presentation/event_details/widgets/event_info_card_widget.dart';
|
||||
import 'package:krow/features/events/presentation/event_details/widgets/shift/shift_widget.dart';
|
||||
import 'package:krow/features/home/presentation/home_screen.dart';
|
||||
import 'package:modal_progress_hud_nsn/modal_progress_hud_nsn.dart';
|
||||
|
||||
import '../../domain/blocs/events_list_bloc/events_bloc.dart';
|
||||
import '../../domain/blocs/events_list_bloc/events_event.dart';
|
||||
|
||||
@RoutePage()
|
||||
class EventDetailsScreen extends StatefulWidget implements AutoRouteWrapper {
|
||||
final EventEntity event;
|
||||
final bool isPreview;
|
||||
|
||||
const EventDetailsScreen(
|
||||
{super.key, required this.event, this.isPreview = false});
|
||||
|
||||
@override
|
||||
State<EventDetailsScreen> createState() => _EventDetailsScreenState();
|
||||
|
||||
@override
|
||||
Widget wrappedRoute(BuildContext context) {
|
||||
return BlocProvider(
|
||||
key: Key(event.id),
|
||||
create: (context) {
|
||||
return EventDetailsBloc(event, getIt<EventsRepository>(),isPreview)
|
||||
..add(EventDetailsInitialEvent());
|
||||
},
|
||||
child: this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EventDetailsScreenState extends State<EventDetailsScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: KwAppBar(
|
||||
titleText: widget.isPreview ? 'Event Preview' : 'Event Details',
|
||||
),
|
||||
body: BlocConsumer<EventDetailsBloc, EventDetailsState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.showErrorPopup != current.showErrorPopup ||
|
||||
previous.needDeepPop != current.needDeepPop,
|
||||
listener: (context, state) {
|
||||
if (state.needDeepPop) {
|
||||
context.router.popUntilRoot();
|
||||
homeContext?.maybePop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.showErrorPopup != null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(state.showErrorPopup ?? ''),
|
||||
));
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return ModalProgressHUD(
|
||||
inAsyncCall: state.inLoading,
|
||||
child: ScrollLayoutHelper(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 24),
|
||||
upperWidget: Column(
|
||||
children: [
|
||||
EventInfoCardWidget(
|
||||
item: state.event,
|
||||
isPreview: widget.isPreview,
|
||||
),
|
||||
EventCompletedByCardWidget(
|
||||
completedBy: state.event.completedBy,
|
||||
completedNote: state.event.completedNode,
|
||||
),
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
primary: false,
|
||||
itemCount: state.shifts.length,
|
||||
itemBuilder: (context, index) {
|
||||
return ShiftWidget(
|
||||
index: index, shiftState: state.shifts[index]);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
lowerWidget: widget.isPreview
|
||||
? _buildPreviewButtons()
|
||||
: const SizedBox.shrink()),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPreviewButtons() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 24),
|
||||
child: KwPopUpButton(
|
||||
label: widget.event.status == null ||
|
||||
widget.event.status == EventStatus.draft
|
||||
? widget.event.id.isEmpty
|
||||
? 'Save as Draft'
|
||||
: 'Update Draft'
|
||||
: 'Save Event',
|
||||
popUpPadding: 16,
|
||||
items: [
|
||||
KwPopUpButtonItem(
|
||||
title: 'Publish Event',
|
||||
onTap: () {
|
||||
BlocProvider.of<EventDetailsBloc>(context)
|
||||
.add(DetailsPublishEvent());
|
||||
},
|
||||
),
|
||||
if (widget.event.status == EventStatus.draft ||
|
||||
widget.event.status == null)
|
||||
KwPopUpButtonItem(
|
||||
title: widget.event.id.isEmpty ? 'Save as Draft' : 'Update Draft',
|
||||
onTap: () {
|
||||
BlocProvider.of<EventDetailsBloc>(context)
|
||||
.add(CreateEventPostEvent());
|
||||
},
|
||||
),
|
||||
KwPopUpButtonItem(
|
||||
title: widget.event.status == null ||
|
||||
widget.event.status == EventStatus.draft
|
||||
? 'Edit Event Draft'
|
||||
: 'Edit Event',
|
||||
onTap: () {
|
||||
context.router.maybePop();
|
||||
},
|
||||
color: AppColors.primaryBlue),
|
||||
if (widget.event.status == EventStatus.draft ||
|
||||
widget.event.status == null)
|
||||
KwPopUpButtonItem(
|
||||
title: 'Delete Event Draft',
|
||||
onTap: () {
|
||||
BlocProvider.of<EventDetailsBloc>(context)
|
||||
.add(DetailsDeleteDraftEvent());
|
||||
BlocProvider.of<EventsBloc>(context)
|
||||
.add(LoadTabEventEvent(tabIndex: 3, subTabIndex: 0));
|
||||
},
|
||||
color: AppColors.statusError),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/application/routing/routes.gr.dart';
|
||||
import 'package:krow/core/entity/event_entity.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/dialogs/kw_dialog.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_input.dart';
|
||||
import 'package:krow/features/events/domain/blocs/details/event_details_bloc.dart';
|
||||
import 'package:krow/features/events/presentation/event_details/widgets/event_qr_popup.dart';
|
||||
|
||||
class EventButtonGroupWidget extends StatelessWidget {
|
||||
final EventEntity item;
|
||||
|
||||
final bool isPreview;
|
||||
|
||||
const EventButtonGroupWidget(
|
||||
{super.key, required this.item, required this.isPreview});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isPreview) return const SizedBox.shrink();
|
||||
|
||||
final now = DateTime.now();
|
||||
final startTime =
|
||||
item.startDate ?? DateTime.now(); // adjust property name if needed
|
||||
final hoursUntilStart = startTime.difference(now).inHours;
|
||||
final showDraftButton = hoursUntilStart >= 24;
|
||||
|
||||
switch (item.status) {
|
||||
case EventStatus.confirmed:
|
||||
return Column(children: [
|
||||
_buildActiveButtonGroup(context),
|
||||
Gap(8),
|
||||
_buildConfirmedButtonGroup(context)
|
||||
]);
|
||||
case EventStatus.active:
|
||||
return _buildActiveButtonGroup(context);
|
||||
case EventStatus.finished:
|
||||
return _buildCompletedButtonGroup(context);
|
||||
case EventStatus.pending:
|
||||
case EventStatus.assigned:
|
||||
return Column(children: [
|
||||
if (showDraftButton) _buildDraftButtonGroup(context),
|
||||
if (showDraftButton) Gap(8),
|
||||
_buildConfirmedButtonGroup(context)
|
||||
]);
|
||||
case EventStatus.draft:
|
||||
return Column(children: [
|
||||
_buildDraftButtonGroup(context)
|
||||
]);
|
||||
|
||||
case EventStatus.completed:
|
||||
case EventStatus.closed:
|
||||
// return _buildClosedButtonGroup();
|
||||
default:
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildActiveButtonGroup(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
KwButton.primary(
|
||||
label: 'QR Code',
|
||||
onPressed: () {
|
||||
EventQrPopup.show(context, item);
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
KwButton.outlinedPrimary(
|
||||
label: 'Clock Manually',
|
||||
onPressed: () {
|
||||
context.router.push(ClockManualRoute(
|
||||
staffContacts: item.shifts
|
||||
?.expand(
|
||||
(s) => s.positions.expand((p) => p.staffContacts))
|
||||
.toList() ??
|
||||
[]));
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildConfirmedButtonGroup(BuildContext context) {
|
||||
return KwButton.outlinedPrimary(
|
||||
label: 'Cancel Event',
|
||||
onPressed: () {
|
||||
BlocProvider.of<EventDetailsBloc>(context).add(CancelClientEvent());
|
||||
},
|
||||
).copyWith(
|
||||
color: AppColors.statusError,
|
||||
textColors: AppColors.statusError,
|
||||
borderColor: AppColors.statusError);
|
||||
}
|
||||
|
||||
Widget _buildDraftButtonGroup(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
KwButton.accent(
|
||||
label: 'Edit Event',
|
||||
onPressed: () async {
|
||||
BlocProvider.of<EventDetailsBloc>(context).add(
|
||||
DisablePollingEvent(),
|
||||
);
|
||||
await context.router.push(
|
||||
CreateEventFlowRoute(children: [
|
||||
CreateEventRoute(
|
||||
eventModel: item.dto,
|
||||
),
|
||||
]),
|
||||
);
|
||||
BlocProvider.of<EventDetailsBloc>(context).add(
|
||||
RefreshEventDetailsEvent(),
|
||||
);
|
||||
BlocProvider.of<EventDetailsBloc>(context).add(
|
||||
EnablePollingEvent(),
|
||||
);
|
||||
},
|
||||
),
|
||||
// Gap(8),
|
||||
// KwButton.outlinedPrimary(
|
||||
// label: 'Delete Event Draft',
|
||||
// onPressed: () {
|
||||
// BlocProvider.of<EventDetailsBloc>(context)
|
||||
// .add(DetailsDeleteDraftEvent());
|
||||
// },
|
||||
// ).copyWith(
|
||||
// color: AppColors.statusError,
|
||||
// textColors: AppColors.statusError,
|
||||
// borderColor: AppColors.statusError),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCompletedButtonGroup(context) {
|
||||
return Column(
|
||||
children: [
|
||||
KwButton.primary(
|
||||
label: 'Complete Event',
|
||||
onPressed: () {
|
||||
_completeEvent(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildClosedButtonGroup() {
|
||||
return Column(
|
||||
children: [
|
||||
KwButton.primary(
|
||||
label: 'View Invoice',
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _completeEvent(BuildContext context) async {
|
||||
var controller = TextEditingController();
|
||||
var result = await KwDialog.show(
|
||||
context: context,
|
||||
icon: Assets.images.icons.navigation.confetti,
|
||||
title: 'Complete Event',
|
||||
message: 'Please tell us how did the event went:',
|
||||
state: KwDialogState.info,
|
||||
child: KwTextInput(
|
||||
controller: controller,
|
||||
maxLength: 300,
|
||||
showCounter: true,
|
||||
minHeight: 144,
|
||||
hintText: 'Enter your note here...',
|
||||
title: 'Note (optional)',
|
||||
),
|
||||
primaryButtonLabel: 'Complete Event',
|
||||
onPrimaryButtonPressed: (dialogContext) {
|
||||
BlocProvider.of<EventDetailsBloc>(context)
|
||||
.add(CompleteEventEvent(comment: controller.text));
|
||||
Navigator.of(dialogContext).pop(true);
|
||||
},
|
||||
secondaryButtonLabel: 'Cancel',
|
||||
);
|
||||
|
||||
if (result) {
|
||||
await KwDialog.show(
|
||||
context: context,
|
||||
icon: Assets.images.icons.navigation.confetti,
|
||||
title: 'Thanks!',
|
||||
message:
|
||||
'Thank you for using our app! We hope you’ve had an awesome event!',
|
||||
primaryButtonLabel: 'Close',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/data/models/event/business_member_model.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
|
||||
class EventCompletedByCardWidget extends StatelessWidget {
|
||||
final BusinessMemberModel? completedBy;
|
||||
final String? completedNote;
|
||||
|
||||
const EventCompletedByCardWidget(
|
||||
{super.key, required this.completedBy, required this.completedNote});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (completedBy == null) return Container();
|
||||
return Container(
|
||||
decoration: KwBoxDecorations.white12,
|
||||
padding: const EdgeInsets.all(12),
|
||||
margin: const EdgeInsets.only(top: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Completed by',
|
||||
style:
|
||||
AppTextStyles.bodySmallReg.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
const Gap(4),
|
||||
_contact(),
|
||||
const Gap(12),
|
||||
Text(
|
||||
'Note',
|
||||
style:
|
||||
AppTextStyles.bodySmallReg.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
const Gap(2),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
completedNote ?? '',
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Container _contact() {
|
||||
return Container(
|
||||
height: 28,
|
||||
padding: const EdgeInsets.only(left: 2, right: 12, top: 2, bottom: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.tintBlue,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
height: 24,
|
||||
width: 24,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.blackGray,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
)),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'${completedBy?.firstName} ${completedBy?.lastName}',
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow/core/application/common/str_extensions.dart';
|
||||
import 'package:krow/core/data/models/event/addon_model.dart';
|
||||
import 'package:krow/core/entity/event_entity.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/icon_row_info_widget.dart';
|
||||
import 'package:krow/features/events/presentation/event_details/widgets/event_button_group_widget.dart';
|
||||
|
||||
class EventInfoCardWidget extends StatelessWidget {
|
||||
final EventEntity item;
|
||||
|
||||
final bool isPreview;
|
||||
|
||||
const EventInfoCardWidget(
|
||||
{required this.item, super.key, this.isPreview = false});
|
||||
|
||||
Color getIconColor(EventStatus? status) {
|
||||
return switch (status) {
|
||||
EventStatus.active || EventStatus.finished => AppColors.statusSuccess,
|
||||
EventStatus.pending ||
|
||||
EventStatus.assigned ||
|
||||
EventStatus.confirmed =>
|
||||
AppColors.primaryBlue,
|
||||
_ => AppColors.statusWarning
|
||||
};
|
||||
}
|
||||
|
||||
Color getIconBgColor(EventStatus? status) {
|
||||
switch (status) {
|
||||
case EventStatus.active:
|
||||
case EventStatus.finished:
|
||||
return AppColors.tintGreen;
|
||||
case EventStatus.pending:
|
||||
case EventStatus.assigned:
|
||||
case EventStatus.confirmed:
|
||||
return AppColors.tintBlue;
|
||||
default:
|
||||
return AppColors.tintOrange;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: KwBoxDecorations.white12,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildStatusRow(),
|
||||
const Gap(24),
|
||||
Text(item.name, style: AppTextStyles.headingH1),
|
||||
const Gap(24),
|
||||
IconRowInfoWidget(
|
||||
icon: Assets.images.icons.calendar.svg(),
|
||||
title: 'Date',
|
||||
value: DateFormat('MM.dd.yyyy')
|
||||
.format(item.startDate ?? DateTime.now()),
|
||||
),
|
||||
const Gap(12),
|
||||
IconRowInfoWidget(
|
||||
icon: Assets.images.icons.location.svg(),
|
||||
title: 'Location',
|
||||
value: item.hub?.name ?? 'Hub Name',
|
||||
),
|
||||
const Gap(12),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: item.totalCost,
|
||||
builder: (context, value, child) {
|
||||
return IconRowInfoWidget(
|
||||
icon: Assets.images.icons.dollarSquare.svg(),
|
||||
title: 'Value',
|
||||
value: '\$${value.toStringAsFixed(2)}',
|
||||
);
|
||||
}),
|
||||
const Gap(24),
|
||||
const Divider(
|
||||
color: AppColors.grayTintStroke,
|
||||
thickness: 1,
|
||||
height: 0,
|
||||
),
|
||||
_buildAddons(item.addons),
|
||||
..._buildAdditionalInfo(),
|
||||
if (!isPreview) const Gap(24),
|
||||
EventButtonGroupWidget(item: item, isPreview: isPreview),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildAdditionalInfo() {
|
||||
return [
|
||||
const Gap(12),
|
||||
Text('Additional Information',
|
||||
style:
|
||||
AppTextStyles.bodySmallReg.copyWith(color: AppColors.blackGray)),
|
||||
const Gap(2),
|
||||
Text(
|
||||
(item.additionalInfo == null || item.additionalInfo!.trim().isEmpty)
|
||||
? 'No additional information'
|
||||
: item.additionalInfo!,
|
||||
style: AppTextStyles.bodyMediumMed),
|
||||
];
|
||||
}
|
||||
|
||||
Widget _buildAddons(List<AddonModel>? selectedAddons) {
|
||||
if (item.addons == null || item.addons!.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
...selectedAddons?.map((addon) {
|
||||
var textStyle = AppTextStyles.bodyMediumMed.copyWith(height: 1);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(addon.name ?? '', style: textStyle),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Assets.images.icons.addonInclude.svg(),
|
||||
const Gap(7),
|
||||
Text('Included', style: textStyle)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}) ??
|
||||
[],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Row _buildStatusRow() {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
height: 48,
|
||||
width: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: getIconBgColor(item.status),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Center(
|
||||
child: Assets.images.icons.navigation.confetti.svg(
|
||||
colorFilter:
|
||||
ColorFilter.mode(getIconColor(item.status), BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!isPreview)
|
||||
Container(
|
||||
height: 28,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: item.status?.color,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2.0),
|
||||
child: Text(
|
||||
item.status?.name.capitalize() ?? '',
|
||||
style: AppTextStyles.bodySmallMed.copyWith(
|
||||
color: item.status == EventStatus.draft
|
||||
? null
|
||||
: AppColors.grayWhite),
|
||||
),
|
||||
),
|
||||
))
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension on EventStatus {
|
||||
Color get color {
|
||||
switch (this) {
|
||||
case EventStatus.active:
|
||||
case EventStatus.finished:
|
||||
return AppColors.statusSuccess;
|
||||
case EventStatus.pending:
|
||||
return AppColors.blackGray;
|
||||
case EventStatus.assigned:
|
||||
return AppColors.statusWarning;
|
||||
case EventStatus.confirmed:
|
||||
return AppColors.primaryBlue;
|
||||
case EventStatus.completed:
|
||||
return AppColors.statusSuccess;
|
||||
case EventStatus.closed:
|
||||
return AppColors.bgColorDark;
|
||||
case EventStatus.canceled:
|
||||
return AppColors.statusError;
|
||||
case EventStatus.draft:
|
||||
return AppColors.primaryYolk;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
import 'package:krow/core/entity/event_entity.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
class EventQrPopup {
|
||||
static Future<void> show(BuildContext context, EventEntity item) async {
|
||||
var qrKey = GlobalKey();
|
||||
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return Center(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
decoration: KwBoxDecorations.white24,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 24, left: 24, right: 24),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Event QR code',
|
||||
style: AppTextStyles.headingH3.copyWith(height: 1),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Assets.images.icons.x.svg(
|
||||
width: 16,
|
||||
height: 16,
|
||||
colorFilter: const ColorFilter.mode(
|
||||
AppColors.blackCaptionText,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
RepaintBoundary(
|
||||
key: qrKey,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 24, right: 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Gap(8),
|
||||
Text(
|
||||
'The QR code below has been successfully generated for the ${item.name}. You can share this code with staff members to enable them to clock in.',
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
const Gap(24),
|
||||
QrImageView(
|
||||
data: jsonEncode({
|
||||
'type': 'event',
|
||||
'birth': 'app',
|
||||
'eventId': item.id
|
||||
}),
|
||||
version: QrVersions.auto,
|
||||
size: MediaQuery.of(context).size.width - 56,
|
||||
),
|
||||
const Gap(24),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 24, left: 24, right: 24),
|
||||
child: KwButton.primary(
|
||||
label: 'Share QR Code',
|
||||
onPressed: () async {
|
||||
final params = ShareParams(
|
||||
text: 'Qr code for ${item.name}',
|
||||
files: [
|
||||
XFile.fromData(
|
||||
await removeAlphaAndReplaceTransparentWithWhite(
|
||||
qrKey.currentContext!.findRenderObject()
|
||||
as RenderRepaintBoundary),
|
||||
name: 'event_qr.png',
|
||||
mimeType: 'image/png',
|
||||
)
|
||||
],
|
||||
);
|
||||
await SharePlus.instance.share(params);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
static Future<Uint8List> removeAlphaAndReplaceTransparentWithWhite(
|
||||
RenderRepaintBoundary boundary) async {
|
||||
final image = await boundary.toImage(pixelRatio: 5.0);
|
||||
final byteData = await image.toByteData(format: ui.ImageByteFormat.rawRgba);
|
||||
final Uint8List rgba = byteData!.buffer.asUint8List();
|
||||
final int length = rgba.lengthInBytes;
|
||||
final Uint8List rgb = Uint8List(length ~/ 4 * 3);
|
||||
|
||||
for (int i = 0, j = 0; i < length; i += 4, j += 3) {
|
||||
int r = rgba[i];
|
||||
int g = rgba[i + 1];
|
||||
int b = rgba[i + 2];
|
||||
int a = rgba[i + 3];
|
||||
|
||||
if (a < 255) {
|
||||
// Replace transparent pixel with white
|
||||
r = 255;
|
||||
g = 255;
|
||||
b = 255;
|
||||
}
|
||||
rgb[j] = r;
|
||||
rgb[j + 1] = g;
|
||||
rgb[j + 2] = b;
|
||||
}
|
||||
|
||||
final width = image.width;
|
||||
final height = image.height;
|
||||
final img.Image rgbImage = img.Image.fromBytes(
|
||||
width: width,
|
||||
height: height,
|
||||
bytes: rgb.buffer,
|
||||
numChannels: 3,
|
||||
format: img.Format.uint8,
|
||||
);
|
||||
return Uint8List.fromList(img.encodePng(rgbImage));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/application/routing/routes.gr.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/assigned_staff_item_widget.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
|
||||
import 'package:krow/features/events/domain/blocs/details/event_details_bloc.dart';
|
||||
|
||||
class AssignedStaff extends StatelessWidget {
|
||||
final PositionState roleState;
|
||||
|
||||
const AssignedStaff({super.key, required this.roleState});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
_buildRoleHeader(context),
|
||||
if (roleState.isStaffExpanded)
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
height: roleState.isStaffExpanded ? null : 0,
|
||||
child: Column(children: [
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
primary: false,
|
||||
itemCount: min(3, roleState.position.staffContacts.length),
|
||||
itemBuilder: (context, index) {
|
||||
return AssignedStaffItemWidget(
|
||||
staffContact: roleState.position.staffContacts[index],
|
||||
department: roleState.position.department?.name ?? '',
|
||||
);
|
||||
}),
|
||||
KwButton.outlinedPrimary(
|
||||
label: 'View All',
|
||||
onPressed: () {
|
||||
context.pushRoute(AssignedStaffRoute(
|
||||
staffContacts: roleState.position.staffContacts,
|
||||
department: roleState.position.department?.name ?? '',
|
||||
));
|
||||
}),
|
||||
const Gap(8),
|
||||
])),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRoleHeader(context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 16, bottom: 16),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
BlocProvider.of<EventDetailsBloc>(context)
|
||||
.add(OnAssignedStaffHeaderTapEvent(roleState));
|
||||
},
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text('Assigned Staff', style: AppTextStyles.bodyLargeMed),
|
||||
Container(
|
||||
height: 16,
|
||||
width: 16,
|
||||
color: Colors.transparent,
|
||||
child: AnimatedRotation(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
turns: roleState.isStaffExpanded ? 0.5 : 0,
|
||||
child: Center(
|
||||
child: Assets.images.icons.chevronDown.svg(
|
||||
colorFilter: ColorFilter.mode(
|
||||
roleState.isStaffExpanded
|
||||
? AppColors.blackBlack
|
||||
: AppColors.blackCaptionText,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/icon_row_info_widget.dart';
|
||||
import 'package:krow/features/events/domain/blocs/details/event_details_bloc.dart';
|
||||
import 'package:krow/features/events/presentation/event_details/widgets/role/asigned_staff.dart';
|
||||
|
||||
class RoleWidget extends StatefulWidget {
|
||||
final PositionState roleState;
|
||||
|
||||
const RoleWidget({super.key, required this.roleState});
|
||||
|
||||
@override
|
||||
State<RoleWidget> createState() => _RoleWidgetState();
|
||||
}
|
||||
|
||||
class _RoleWidgetState extends State<RoleWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var eventDate =
|
||||
widget.roleState.position.parentShift?.parentEvent?.startDate ??
|
||||
DateTime.now();
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
padding: const EdgeInsets.only(left: 12, right: 12),
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
decoration: KwBoxDecorations.primaryLight12,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildRoleHeader(context),
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
alignment: Alignment.topCenter,
|
||||
child: Container(
|
||||
height: widget.roleState.isExpanded ? null : 0,
|
||||
decoration: const BoxDecoration(),
|
||||
child: Column(
|
||||
children: [
|
||||
IconRowInfoWidget(
|
||||
icon: Assets.images.icons.data.svg(),
|
||||
title: 'Department',
|
||||
value: widget.roleState.position.department?.name ?? ''),
|
||||
const Gap(16),
|
||||
IconRowInfoWidget(
|
||||
icon: Assets.images.icons.profile2user.svg(),
|
||||
title: 'Number of Employee for one Role',
|
||||
value: '${widget.roleState.position.count} persons'),
|
||||
const Gap(16),
|
||||
IconRowInfoWidget(
|
||||
icon: Assets.images.icons.calendar.svg(),
|
||||
title: 'Start Date & Time',
|
||||
value: DateFormat('MM.dd.yyyy, hh:mm a').format(
|
||||
eventDate.copyWith(
|
||||
hour: widget.roleState.position.startTime.hour,
|
||||
minute:
|
||||
widget.roleState.position.startTime.minute))),
|
||||
const Gap(16),
|
||||
IconRowInfoWidget(
|
||||
icon: Assets.images.icons.calendar.svg(),
|
||||
title: 'End Date & Time',
|
||||
value: DateFormat('MM.dd.yyyy, hh:mm a').format(
|
||||
eventDate.copyWith(
|
||||
hour: widget.roleState.position.endTime.hour,
|
||||
minute:
|
||||
widget.roleState.position.endTime.minute))),
|
||||
const Gap(16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: IconRowInfoWidget(
|
||||
icon: Assets.images.icons.dollarSquare.svg(),
|
||||
title: 'Value',
|
||||
value: '\$${widget.roleState.position.price.toStringAsFixed(2)}'),
|
||||
),
|
||||
if (widget.roleState.position.staffContacts.isNotEmpty) ...[
|
||||
const Gap(16),
|
||||
const Divider(
|
||||
color: AppColors.grayTintStroke,
|
||||
thickness: 1,
|
||||
height: 0),
|
||||
AssignedStaff(
|
||||
roleState: widget.roleState,
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRoleHeader(context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
BlocProvider.of<EventDetailsBloc>(context)
|
||||
.add(OnRoleHeaderTapEvent(widget.roleState));
|
||||
},
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 24, bottom: 24),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(widget.roleState.position.businessSkill?.skill?.name ?? '',
|
||||
style: AppTextStyles.headingH3),
|
||||
Container(
|
||||
height: 16,
|
||||
width: 16,
|
||||
color: Colors.transparent,
|
||||
child: AnimatedRotation(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
turns: widget.roleState.isExpanded ? 0.5 : 0,
|
||||
child: Center(
|
||||
child: Assets.images.icons.chevronDown.svg(
|
||||
colorFilter: ColorFilter.mode(
|
||||
widget.roleState.isExpanded
|
||||
? AppColors.blackBlack
|
||||
: AppColors.blackCaptionText,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/data/models/event/business_member_model.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/icon_row_info_widget.dart';
|
||||
import 'package:krow/features/events/domain/blocs/details/event_details_bloc.dart';
|
||||
import 'package:krow/features/events/presentation/event_details/widgets/role/role_widget.dart';
|
||||
|
||||
class ShiftWidget extends StatefulWidget {
|
||||
final int index;
|
||||
final ShiftState shiftState;
|
||||
|
||||
const ShiftWidget({super.key, required this.index, required this.shiftState});
|
||||
|
||||
@override
|
||||
State<ShiftWidget> createState() => _ShiftWidgetState();
|
||||
}
|
||||
|
||||
class _ShiftWidgetState extends State<ShiftWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
padding: const EdgeInsets.only(left: 12, right: 12),
|
||||
margin: const EdgeInsets.only(top: 12),
|
||||
decoration: KwBoxDecorations.white12,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildShiftHeader(context),
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
alignment: Alignment.topCenter,
|
||||
child: Container(
|
||||
height: widget.shiftState.isExpanded ? null : 0,
|
||||
decoration: const BoxDecoration(),
|
||||
child: Column(
|
||||
children: [
|
||||
IconRowInfoWidget(
|
||||
icon: Assets.images.icons.location.svg(),
|
||||
title: 'Address',
|
||||
value: widget
|
||||
.shiftState.shift.fullAddress?.formattedAddress ??
|
||||
''),
|
||||
const Gap(16),
|
||||
const Divider(
|
||||
color: AppColors.grayTintStroke, thickness: 1, height: 0),
|
||||
const Gap(16),
|
||||
Stack(
|
||||
children: [
|
||||
IconRowInfoWidget(
|
||||
icon: Assets.images.icons.userTag.svg(),
|
||||
title: 'Shift Contact',
|
||||
value: 'Manager'),
|
||||
if (widget.shiftState.shift.managers.isNotEmpty)
|
||||
Positioned(
|
||||
bottom: 4,
|
||||
top: 4,
|
||||
right: 0,
|
||||
child: _contact(
|
||||
widget.shiftState.shift.managers.first))
|
||||
],
|
||||
),
|
||||
const Gap(16),
|
||||
ListView.builder(
|
||||
itemCount: widget.shiftState.positions.length,
|
||||
primary: false,
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (_, index) {
|
||||
return RoleWidget(
|
||||
roleState: widget.shiftState.positions[index],
|
||||
);
|
||||
},
|
||||
),
|
||||
const Gap(12),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildShiftHeader(context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
BlocProvider.of<EventDetailsBloc>(context)
|
||||
.add(OnShiftHeaderTapEvent(widget.shiftState));
|
||||
},
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 24, bottom: 24),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Shift Details #${widget.index + 1}',
|
||||
style: AppTextStyles.headingH1),
|
||||
Container(
|
||||
height: 40,
|
||||
width: 40,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: AppColors.grayTintStroke,
|
||||
),
|
||||
),
|
||||
child: AnimatedRotation(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
turns: widget.shiftState.isExpanded ? 0.5 : 0,
|
||||
child: Center(
|
||||
child: Assets.images.icons.chevronDown.svg(
|
||||
colorFilter: ColorFilter.mode(
|
||||
widget.shiftState.isExpanded
|
||||
? AppColors.blackBlack
|
||||
: AppColors.blackCaptionText,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Container _contact(BusinessMemberModel contact) {
|
||||
return Container(
|
||||
height: 28,
|
||||
padding: const EdgeInsets.only(left: 2, right: 12, top: 2, bottom: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.tintBlue,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
height: 24,
|
||||
width: 24,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.blackGray,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
)),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'${contact.firstName} ${contact.lastName}',
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow/core/application/common/str_extensions.dart';
|
||||
import 'package:krow/core/application/routing/routes.gr.dart';
|
||||
import 'package:krow/core/entity/event_entity.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/icon_row_info_widget.dart';
|
||||
|
||||
class EventListItemWidget extends StatelessWidget {
|
||||
final EventEntity item;
|
||||
|
||||
const EventListItemWidget({required this.item, super.key});
|
||||
|
||||
Color getIconColor(EventStatus? status) {
|
||||
switch (status) {
|
||||
case EventStatus.active:
|
||||
case EventStatus.finished:
|
||||
return AppColors.statusSuccess;
|
||||
case EventStatus.pending:
|
||||
case EventStatus.assigned:
|
||||
case EventStatus.confirmed:
|
||||
return AppColors.primaryBlue;
|
||||
default:
|
||||
return AppColors.statusWarning;
|
||||
}
|
||||
}
|
||||
|
||||
Color getIconBgColor(EventStatus? status) {
|
||||
switch (status) {
|
||||
case EventStatus.active:
|
||||
case EventStatus.finished:
|
||||
return AppColors.tintGreen;
|
||||
case EventStatus.pending:
|
||||
case EventStatus.assigned:
|
||||
case EventStatus.confirmed:
|
||||
return AppColors.tintBlue;
|
||||
default:
|
||||
return AppColors.tintOrange;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
context.router.push(EventDetailsRoute(event: item));
|
||||
},
|
||||
child: Container(
|
||||
decoration: KwBoxDecorations.white12,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
|
||||
margin: const EdgeInsets.only(bottom: 12, left: 16, right: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildStatusRow(),
|
||||
const Gap(12),
|
||||
Text(item.name, style: AppTextStyles.headingH1),
|
||||
const Gap(4),
|
||||
Text('BEO-${item.id}',
|
||||
style: AppTextStyles.bodyMediumReg
|
||||
.copyWith(color: AppColors.blackGray)),
|
||||
const Gap(12),
|
||||
IconRowInfoWidget(
|
||||
icon: Assets.images.icons.calendar.svg(),
|
||||
title: 'Date',
|
||||
value: DateFormat('MM.dd.yyyy')
|
||||
.format(item.startDate ?? DateTime.now()),
|
||||
),
|
||||
const Gap(12),
|
||||
IconRowInfoWidget(
|
||||
icon: Assets.images.icons.location.svg(),
|
||||
title: 'Location',
|
||||
value: item.hub?.name ?? 'Hub Name',
|
||||
),
|
||||
const Gap(24),
|
||||
Container(
|
||||
height: 34,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: KwBoxDecorations.primaryLight8.copyWith(),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Approximate Total Costs',
|
||||
style: AppTextStyles.bodyMediumReg
|
||||
.copyWith(color: AppColors.blackGray)),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: item.totalCost,
|
||||
builder: (context, value, child) {
|
||||
return Text(
|
||||
'\$${value.toStringAsFixed(2)}',
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
);
|
||||
})
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Row _buildStatusRow() {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
height: 48,
|
||||
width: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: getIconBgColor(item?.status),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Center(
|
||||
child: Assets.images.icons.navigation.confetti.svg(
|
||||
colorFilter:
|
||||
ColorFilter.mode(getIconColor(item.status), BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 28,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: item.status?.color,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2.0),
|
||||
child: Text(
|
||||
item.status?.name.capitalize() ?? '',
|
||||
style: AppTextStyles.bodySmallMed.copyWith(
|
||||
color: item.status == EventStatus.draft
|
||||
? null
|
||||
: AppColors.grayWhite),
|
||||
),
|
||||
),
|
||||
))
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension on EventStatus {
|
||||
Color get color {
|
||||
switch (this) {
|
||||
case EventStatus.active:
|
||||
case EventStatus.finished:
|
||||
return AppColors.statusSuccess;
|
||||
case EventStatus.pending:
|
||||
return AppColors.blackGray;
|
||||
case EventStatus.assigned:
|
||||
return AppColors.statusWarning;
|
||||
case EventStatus.confirmed:
|
||||
return AppColors.primaryBlue;
|
||||
case EventStatus.completed:
|
||||
return AppColors.statusSuccess;
|
||||
case EventStatus.closed:
|
||||
return AppColors.bgColorDark;
|
||||
case EventStatus.canceled:
|
||||
return AppColors.statusError;
|
||||
case EventStatus.draft:
|
||||
return AppColors.primaryYolk;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/entity/event_entity.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/scroll_layout_helper.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_app_bar.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_option_selector.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_tabs.dart';
|
||||
import 'package:krow/features/events/domain/blocs/events_list_bloc/events_bloc.dart';
|
||||
import 'package:krow/features/events/domain/blocs/events_list_bloc/events_event.dart';
|
||||
import 'package:krow/features/events/domain/blocs/events_list_bloc/events_state.dart';
|
||||
import 'package:krow/features/events/presentation/lists/event_list_item_widget.dart';
|
||||
import 'package:modal_progress_hud_nsn/modal_progress_hud_nsn.dart';
|
||||
|
||||
@RoutePage()
|
||||
class EventsListMainScreen extends StatefulWidget implements AutoRouteWrapper {
|
||||
const EventsListMainScreen({super.key});
|
||||
|
||||
@override
|
||||
State<EventsListMainScreen> createState() => _EventsListMainScreenState();
|
||||
|
||||
@override
|
||||
Widget wrappedRoute(BuildContext context) {
|
||||
return BlocProvider<EventsBloc>(
|
||||
create: (context) => EventsBloc()..add(const EventsInitialEvent()),
|
||||
child: this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EventsListMainScreenState extends State<EventsListMainScreen> {
|
||||
final tabs = <String, List<String>>{
|
||||
'Upcoming': ['Pending', 'Assigned', 'Confirmed'],
|
||||
'Active': ['Ongoing', 'Finished'],
|
||||
'Past': ['Completed', 'Closed', 'Canceled'],
|
||||
'Drafts': [],
|
||||
};
|
||||
|
||||
late ScrollController _scrollController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController = ScrollController();
|
||||
_scrollController.addListener(_onScroll);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.removeListener(_onScroll);
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
if (_scrollController.position.atEdge) {
|
||||
if (_scrollController.position.pixels != 0) {
|
||||
BlocProvider.of<EventsBloc>(context).add(
|
||||
LoadMoreEventEvent(
|
||||
tabIndex: BlocProvider.of<EventsBloc>(context).state.tabIndex,
|
||||
subTabIndex:
|
||||
BlocProvider.of<EventsBloc>(context).state.subTabIndex),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<EventsBloc, EventsState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.errorMessage != current.errorMessage,
|
||||
listener: (context, state) {
|
||||
if (state.errorMessage != null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(state.errorMessage!),
|
||||
));
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
var tabState = state.tabs[state.tabIndex]![state.subTabIndex]!;
|
||||
List<EventEntity> items = tabState.items;
|
||||
|
||||
return Scaffold(
|
||||
appBar: KwAppBar(
|
||||
titleText: 'Events',
|
||||
centerTitle: false,
|
||||
),
|
||||
body: ModalProgressHUD(
|
||||
inAsyncCall: tabState.inLoading && tabState.items.isNotEmpty,
|
||||
child: ScrollLayoutHelper(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
onRefresh: () async {
|
||||
BlocProvider.of<EventsBloc>(context).add(LoadTabEventEvent(
|
||||
tabIndex: state.tabIndex, subTabIndex: state.subTabIndex));
|
||||
},
|
||||
controller: _scrollController,
|
||||
upperWidget: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
KwTabBar(
|
||||
tabs: tabs.keys.toList(),
|
||||
flexes: const [7, 5, 5, 5],
|
||||
onTap: (index) {
|
||||
BlocProvider.of<EventsBloc>(context)
|
||||
.add(EventsTabChangedEvent(tabIndex: index));
|
||||
}),
|
||||
const Gap(24),
|
||||
_buildSubTab(state, context),
|
||||
if (tabState.inLoading && tabState.items.isEmpty)
|
||||
..._buildListLoading(),
|
||||
if (!tabState.inLoading && items.isEmpty)
|
||||
..._emptyListWidget(),
|
||||
RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
BlocProvider.of<EventsBloc>(context).add(
|
||||
LoadTabEventEvent(
|
||||
tabIndex: state.tabIndex,
|
||||
subTabIndex: state.subTabIndex));
|
||||
},
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
return EventListItemWidget(
|
||||
item: items[index],
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
lowerWidget: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubTab(EventsState state, BuildContext context) {
|
||||
if (state.tabs[state.tabIndex]!.length > 1) {
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 25.5,
|
||||
child: Container(
|
||||
height: 1,
|
||||
color: AppColors.blackGray,
|
||||
)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 24, left: 16, right: 16),
|
||||
child: KwOptionSelector(
|
||||
selectedIndex: state.subTabIndex,
|
||||
onChanged: (index) {
|
||||
BlocProvider.of<EventsBloc>(context)
|
||||
.add(EventsSubTabChangedEvent(subTabIndex: index));
|
||||
},
|
||||
height: 26,
|
||||
selectorHeight: 4,
|
||||
textStyle: AppTextStyles.bodyMediumReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
selectedTextStyle: AppTextStyles.bodyMediumMed,
|
||||
itemAlign: Alignment.topCenter,
|
||||
items: tabs[tabs.keys.toList()[state.tabIndex]]!),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> _buildListLoading() {
|
||||
return [
|
||||
const Gap(116),
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
];
|
||||
}
|
||||
|
||||
List<Widget> _emptyListWidget() {
|
||||
return [
|
||||
const Gap(100),
|
||||
Container(
|
||||
height: 64,
|
||||
width: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.grayWhite,
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
),
|
||||
child: Center(child: Assets.images.icons.xCircle.svg()),
|
||||
),
|
||||
const Gap(24),
|
||||
const Text(
|
||||
'You currently have no event',
|
||||
textAlign: TextAlign.center,
|
||||
style: AppTextStyles.headingH2,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/application/routing/routes.gr.dart';
|
||||
import 'package:krow/core/data/models/event/event_model.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';
|
||||
|
||||
class CreateEventPopup extends StatefulWidget {
|
||||
const CreateEventPopup({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.controller,
|
||||
required this.opacity,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final ListenableOverlayPortalController controller;
|
||||
|
||||
final double opacity;
|
||||
|
||||
@override
|
||||
State<CreateEventPopup> createState() => _CreateEventPopupState();
|
||||
}
|
||||
|
||||
class _CreateEventPopupState extends State<CreateEventPopup> {
|
||||
late final ListenableOverlayPortalController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_controller = widget.controller;
|
||||
|
||||
_controller.addListener(() {
|
||||
try {
|
||||
if (context.mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return OverlayPortal(
|
||||
controller: _controller,
|
||||
overlayChildBuilder: (context) {
|
||||
return AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
opacity: widget.opacity,
|
||||
child: AnimatedScale(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
scale: 0.1 + 0.9 * widget.opacity,
|
||||
alignment: const Alignment(0, 0.8),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
_controller.hide();
|
||||
},
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 125),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// _buildItem('Create Recurrent Event', () {
|
||||
// context.router.push(CreateEventFlowRoute(
|
||||
// children: [
|
||||
// CreateEventRoute(
|
||||
// eventType: EventScheduleType.recurring)
|
||||
// ]));
|
||||
// }),
|
||||
// const Gap(8),
|
||||
_buildItem('Create Event', () {
|
||||
context.router.push(CreateEventFlowRoute(
|
||||
children: [
|
||||
CreateEventRoute(
|
||||
eventType: EventScheduleType.oneTime)
|
||||
]));
|
||||
}),
|
||||
const Gap(6),
|
||||
Assets.images.icons.navigation.menuArrow.svg(),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItem(String title, VoidCallback onTap) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
onTap();
|
||||
_controller.hide();
|
||||
},
|
||||
child: Container(
|
||||
width: 215,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.buttonPrimaryYellowDrop,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Text(
|
||||
title,
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (context.mounted) _controller.hide();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class ListenableOverlayPortalController extends OverlayPortalController {
|
||||
List<VoidCallback> listeners = [];
|
||||
Future<void> Function()? onShow;
|
||||
Future<void> Function()? onHide;
|
||||
|
||||
ListenableOverlayPortalController();
|
||||
|
||||
addOnShowListener(Future<void> Function() listener) {
|
||||
onShow = listener;
|
||||
}
|
||||
|
||||
addOnHideListener(Future<void> Function() listener) {
|
||||
onHide = listener;
|
||||
}
|
||||
|
||||
@override
|
||||
void show() async {
|
||||
super.show();
|
||||
try {
|
||||
for (var element in listeners) {
|
||||
element();
|
||||
}
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (onShow != null) {
|
||||
await onShow!();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void hide() async {
|
||||
if (onHide != null) {
|
||||
await onHide!();
|
||||
}
|
||||
try {
|
||||
super.hide();
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
for (var element in listeners) {
|
||||
try {
|
||||
element();
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void addListener(VoidCallback listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/application/di/injectable.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/ui_kit/kw_button.dart';
|
||||
import 'package:krow/core/sevices/app_update_service.dart';
|
||||
import 'package:krow/features/home/presentation/create_event_widget.dart';
|
||||
|
||||
import '../../notificatins/domain/bloc/notifications_bloc.dart';
|
||||
|
||||
BuildContext? _nestedContext;
|
||||
BuildContext? homeContext;
|
||||
|
||||
@RoutePage()
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@override
|
||||
State<HomeScreen> createState() => _HomeScreenState();
|
||||
}
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen> {
|
||||
final ListenableOverlayPortalController _controller =
|
||||
ListenableOverlayPortalController();
|
||||
|
||||
final List<PageRouteInfo> _routes = const [
|
||||
EventsFlowRoute(),
|
||||
InvoiceFlowRoute(),
|
||||
NotificationFlowRoute(),
|
||||
ProfileFlowRoute(),
|
||||
];
|
||||
|
||||
bool _shouldHideBottomNavBar(BuildContext nestedContext) {
|
||||
final currentPath = context.router.currentPath;
|
||||
return _allowBottomNavBarRoutes
|
||||
.any((route) => currentPath == route.toString());
|
||||
}
|
||||
|
||||
final List<String> _allowBottomNavBarRoutes = [
|
||||
'/home/events/list',
|
||||
'/home/invoice/list',
|
||||
'/home/notifications/list',
|
||||
'/home/profile/preview',
|
||||
];
|
||||
|
||||
double opacity = 0.0;
|
||||
|
||||
getMediaQueryData(BuildContext context) {
|
||||
if (!_shouldHideBottomNavBar(context)) {
|
||||
return MediaQuery.of(context);
|
||||
} else {
|
||||
//76 - navigation height, 22 - bottom padding
|
||||
var newBottomPadding = MediaQuery.of(context).padding.bottom + 76 + 22;
|
||||
var newPadding =
|
||||
MediaQuery.of(context).padding.copyWith(bottom: newBottomPadding);
|
||||
return MediaQuery.of(context).copyWith(padding: newPadding);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller.addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
_controller.addOnHideListener(_hide);
|
||||
_controller.addOnShowListener(_show);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((call) {
|
||||
getIt<AppUpdateService>().checkForUpdate(context);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
homeContext = context;
|
||||
return BlocProvider(
|
||||
create: (context) => NotificationsBloc()..add(NotificationsInitEvent()),
|
||||
child: Scaffold(
|
||||
body: AutoTabsRouter(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
routes: _routes,
|
||||
builder: (context, child) {
|
||||
_nestedContext = context;
|
||||
return Stack(
|
||||
children: [
|
||||
MediaQuery(
|
||||
data: getMediaQueryData(context),
|
||||
child: child,
|
||||
),
|
||||
_buildBarrier(),
|
||||
if (_shouldHideBottomNavBar(context)) _bottomNavBar(context),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBarrier() {
|
||||
return IgnorePointer(
|
||||
ignoring: opacity == 0.0,
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
opacity: opacity,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
_controller.hide();
|
||||
},
|
||||
child: const SizedBox(
|
||||
height: double.maxFinite,
|
||||
width: double.maxFinite,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black38,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _bottomNavBar(context) {
|
||||
final tabsRouter = AutoTabsRouter.of(context);
|
||||
return Positioned(
|
||||
bottom: 22,
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Container(
|
||||
height: 76,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(38),
|
||||
color: AppColors.bgColorDark,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_navBarItem('Events', Assets.images.icons.navigation.confetti, 0,
|
||||
tabsRouter),
|
||||
_navBarItem('Invoice', Assets.images.icons.navigation.emptyWallet,
|
||||
1, tabsRouter),
|
||||
Gap(8),
|
||||
_createEventButton(),
|
||||
Gap(8),
|
||||
_navBarItem('Alerts', Assets.images.icons.appBar.notification, 2,
|
||||
tabsRouter),
|
||||
_navBarItem('Profile', Assets.images.icons.navigation.profile, 3,
|
||||
tabsRouter),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _navBarItem(
|
||||
String text,
|
||||
SvgGenImage icon,
|
||||
int index,
|
||||
tabsRouter,
|
||||
) {
|
||||
var color = tabsRouter.activeIndex == index
|
||||
? AppColors.grayWhite
|
||||
: AppColors.tintDarkBlue;
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if(index<0) return;
|
||||
if (_controller.isShowing) {
|
||||
_controller.hide();
|
||||
}
|
||||
tabsRouter.setActiveIndex(index);
|
||||
},
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
icon.svg(
|
||||
colorFilter: ColorFilter.mode(
|
||||
color,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
text,
|
||||
style: (tabsRouter.activeIndex == index
|
||||
? AppTextStyles.bodyTinyMed
|
||||
: AppTextStyles.bodyTinyReg)
|
||||
.copyWith(color: color),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_createEventButton() {
|
||||
return CreateEventPopup(
|
||||
opacity: opacity,
|
||||
controller: _controller,
|
||||
child: KwButton.accent(
|
||||
onPressed: () {
|
||||
if (_controller.isShowing) {
|
||||
_controller.hide();
|
||||
} else {
|
||||
_controller.show();
|
||||
}
|
||||
},
|
||||
fit: KwButtonFit.circular,
|
||||
iconSize: 24,
|
||||
leftIcon: Assets.images.icons.navigation.noteFavorite,
|
||||
).copyWith(
|
||||
originalIconsColors: true,
|
||||
color: _controller.isShowing
|
||||
? AppColors.buttonPrimaryYellowDrop
|
||||
: null));
|
||||
}
|
||||
|
||||
Future<void> _hide() async {
|
||||
setState(() {
|
||||
opacity = 0.0;
|
||||
});
|
||||
await Future.delayed(const Duration(milliseconds: 200), () {});
|
||||
}
|
||||
|
||||
Future<void> _show() async {
|
||||
WidgetsBinding.instance.addPostFrameCallback((call) {
|
||||
setState(() {
|
||||
opacity = 1.0;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import 'package:graphql_flutter/graphql_flutter.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:krow/core/application/clients/api/api_client.dart';
|
||||
import 'package:krow/core/data/models/pagination_wrapper/pagination_wrapper.dart';
|
||||
|
||||
import 'invoices_gql.dart';
|
||||
import 'models/invoice_model.dart';
|
||||
|
||||
@Injectable()
|
||||
class InvoiceApiProvider {
|
||||
final ApiClient _client;
|
||||
|
||||
InvoiceApiProvider({required ApiClient client}) : _client = client;
|
||||
|
||||
Future<PaginationWrapper> fetchInvoices(String? status,
|
||||
{String? after}) async {
|
||||
final QueryResult result = await _client.query(
|
||||
schema: getInvoicesQuery,
|
||||
body: {
|
||||
if (status != null) 'status': status,
|
||||
'first': 20,
|
||||
'after': after
|
||||
});
|
||||
|
||||
if (result.hasException) {
|
||||
throw Exception(result.exception.toString());
|
||||
}
|
||||
return PaginationWrapper.fromJson(
|
||||
result.data!['client_invoices'], (json) => InvoiceModel.fromJson(json));
|
||||
}
|
||||
|
||||
Future<void>approveInvoice({required String invoiceId}) async{
|
||||
final QueryResult result = await _client.mutate(
|
||||
schema: approveInvoiceMutation,
|
||||
body: {
|
||||
'id': invoiceId,
|
||||
});
|
||||
|
||||
if (result.hasException) {
|
||||
throw Exception(result.exception.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> disputeInvoice({
|
||||
required String invoiceId,
|
||||
required String reason,
|
||||
required String comment,
|
||||
}) async {
|
||||
final QueryResult result = await _client.mutate(
|
||||
schema: disputeInvoiceMutation,
|
||||
body: {
|
||||
'id': invoiceId,
|
||||
'reason': reason,
|
||||
'comment': comment,
|
||||
});
|
||||
|
||||
if (result.hasException) {
|
||||
throw Exception(result.exception.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
const String getInvoicesQuery = '''
|
||||
query GetInvoices (\$status: InvoiceStatus, \$first: Int!, \$after: String) {
|
||||
client_invoices( status: \$status, first: \$first, after: \$after) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
startCursor
|
||||
endCursor
|
||||
total
|
||||
count
|
||||
currentPage
|
||||
lastPage
|
||||
}
|
||||
edges {
|
||||
...invoice
|
||||
cursor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fragment invoice on InvoiceEdge {
|
||||
node {
|
||||
id
|
||||
business {
|
||||
id
|
||||
name
|
||||
avatar
|
||||
registration
|
||||
contact{
|
||||
id
|
||||
first_name
|
||||
last_name
|
||||
email
|
||||
phone
|
||||
}
|
||||
}
|
||||
event {
|
||||
id
|
||||
status
|
||||
start_time
|
||||
end_time
|
||||
contract_type
|
||||
...hub
|
||||
name
|
||||
date
|
||||
purchase_order
|
||||
}
|
||||
status
|
||||
contract_type
|
||||
contract_value
|
||||
total
|
||||
addons_amount
|
||||
work_amount
|
||||
issued_at
|
||||
due_at
|
||||
...items
|
||||
dispute {
|
||||
id
|
||||
reason
|
||||
details
|
||||
support_note
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment items on Invoice {
|
||||
items {
|
||||
id
|
||||
staff {
|
||||
id
|
||||
first_name
|
||||
last_name
|
||||
email
|
||||
phone
|
||||
address
|
||||
}
|
||||
position {
|
||||
id
|
||||
start_time
|
||||
end_time
|
||||
break
|
||||
count
|
||||
rate
|
||||
...business_skill
|
||||
staff{
|
||||
id
|
||||
pivot {
|
||||
id
|
||||
status
|
||||
start_at
|
||||
end_at
|
||||
clock_in
|
||||
clock_out
|
||||
}
|
||||
}
|
||||
}
|
||||
work_hours
|
||||
rate
|
||||
addons_amount
|
||||
work_amount
|
||||
total_amount
|
||||
}
|
||||
}
|
||||
|
||||
fragment business_skill on EventShiftPosition {
|
||||
business_skill {
|
||||
id
|
||||
skill {
|
||||
id
|
||||
name
|
||||
slug
|
||||
}
|
||||
price
|
||||
is_active
|
||||
}
|
||||
}
|
||||
|
||||
fragment hub on Event {
|
||||
hub {
|
||||
id
|
||||
name
|
||||
full_address {
|
||||
formatted_address
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
''';
|
||||
|
||||
const String approveInvoiceMutation = '''
|
||||
mutation ApproveInvoice(\$id: ID!) {
|
||||
confirm_client_invoice(id: \$id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String disputeInvoiceMutation = '''
|
||||
mutation DisputeInvoice(\$id: ID!, \$reason: String!, \$comment: String!) {
|
||||
dispute_client_invoice(id: \$id, reason: \$reason, details: \$comment) {
|
||||
id
|
||||
}
|
||||
}
|
||||
''';
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:krow/features/invoice/data/invoices_api_provider.dart';
|
||||
import 'package:krow/features/invoice/domain/invoice_entity.dart';
|
||||
import 'package:krow/features/invoice/domain/invoices_repository.dart';
|
||||
|
||||
@Singleton(as: InvoicesRepository)
|
||||
class InvoicesRepositoryImpl extends InvoicesRepository {
|
||||
final InvoiceApiProvider _apiProvider;
|
||||
StreamController<InvoiceStatusFilterType>? _statusController;
|
||||
|
||||
InvoicesRepositoryImpl({required InvoiceApiProvider apiProvider})
|
||||
: _apiProvider = apiProvider;
|
||||
|
||||
@override
|
||||
Stream<InvoiceStatusFilterType> get statusStream {
|
||||
_statusController ??= StreamController<InvoiceStatusFilterType>.broadcast();
|
||||
return _statusController!.stream;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_statusController?.close();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<InvoiceListEntity>> getInvoices(
|
||||
{String? lastItemId,
|
||||
required InvoiceStatusFilterType statusFilter}) async {
|
||||
var paginationWrapper = await _apiProvider.fetchInvoices(
|
||||
statusFilter == InvoiceStatusFilterType.all ? null : statusFilter.name,
|
||||
after: lastItemId);
|
||||
return paginationWrapper.edges.map((e) {
|
||||
return InvoiceListEntity.fromModel(
|
||||
e.node,
|
||||
cursor: (paginationWrapper.pageInfo.hasNextPage) ? e.cursor : null,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> approveInvoice({required String invoiceId}) async{
|
||||
await _apiProvider.approveInvoice(invoiceId: invoiceId).then((value) {
|
||||
_statusController?.add(InvoiceStatusFilterType.verified);
|
||||
});
|
||||
_statusController?.add(InvoiceStatusFilterType.all);
|
||||
_statusController?.add(InvoiceStatusFilterType.open);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> disputeInvoice({
|
||||
required String invoiceId,
|
||||
required String reason,
|
||||
required String comment,
|
||||
}) async {
|
||||
await _apiProvider.disputeInvoice(
|
||||
invoiceId: invoiceId,
|
||||
reason: reason,
|
||||
comment: comment,
|
||||
);
|
||||
_statusController?.add(InvoiceStatusFilterType.disputed);
|
||||
_statusController?.add(InvoiceStatusFilterType.all);
|
||||
_statusController?.add(InvoiceStatusFilterType.open);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'invoice_decline_model.g.dart';
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class InvoiceDeclineModel{
|
||||
String id;
|
||||
String? reason;
|
||||
String? details;
|
||||
String? supportNote;
|
||||
|
||||
InvoiceDeclineModel({
|
||||
required this.id,
|
||||
required this.reason,
|
||||
this.details,
|
||||
this.supportNote,
|
||||
});
|
||||
factory InvoiceDeclineModel.fromJson(Map<String, dynamic> json) {
|
||||
return _$InvoiceDeclineModelFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$InvoiceDeclineModelToJson(this);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:krow/core/data/models/shift/event_shift_position_model.dart';
|
||||
import 'package:krow/core/data/models/staff/staff_model.dart';
|
||||
import 'package:krow/core/entity/staff_contact_entity.dart';
|
||||
import 'package:krow/features/events/presentation/event_details/widgets/role/asigned_staff.dart';
|
||||
|
||||
part 'invoice_item_model.g.dart';
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class InvoiceItemModel {
|
||||
final String id;
|
||||
final StaffModel? staff;
|
||||
final EventShiftPositionModel? position;
|
||||
final double? workHours;
|
||||
final double? rate;
|
||||
final double? addonsAmount;
|
||||
final double? workAmount;
|
||||
final double? totalAmount;
|
||||
|
||||
InvoiceItemModel(
|
||||
{required this.id,
|
||||
required this.staff,
|
||||
required this.position,
|
||||
required this.workHours,
|
||||
required this.rate,
|
||||
required this.addonsAmount,
|
||||
required this.workAmount,
|
||||
required this.totalAmount});
|
||||
|
||||
factory InvoiceItemModel.fromJson(Map<String, dynamic> json) {
|
||||
return _$InvoiceItemModelFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$InvoiceItemModelToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:krow/core/data/models/event/business_model.dart';
|
||||
import 'package:krow/core/data/models/event/event_model.dart';
|
||||
import 'package:krow/features/invoice/data/models/invoice_decline_model.dart';
|
||||
import 'package:krow/features/invoice/data/models/invoice_item_model.dart';
|
||||
|
||||
part 'invoice_model.g.dart';
|
||||
|
||||
enum InvoiceStatus { open, disputed, resolved, paid, overdue, verified }
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class InvoiceModel {
|
||||
final String id;
|
||||
final InvoiceStatus status;
|
||||
final BusinessModel? business;
|
||||
final EventModel? event;
|
||||
final String? contractType;
|
||||
final String? contractValue;
|
||||
final String? dueAt;
|
||||
final double? total;
|
||||
final double? addonsAmount;
|
||||
final double? workAmount;
|
||||
final List<InvoiceItemModel>? items;
|
||||
final InvoiceDeclineModel? dispute;
|
||||
|
||||
factory InvoiceModel.fromJson(Map<String, dynamic> json) {
|
||||
return _$InvoiceModelFromJson(json);
|
||||
}
|
||||
|
||||
InvoiceModel(
|
||||
{required this.id,
|
||||
required this.status,
|
||||
required this.business,
|
||||
required this.event,
|
||||
required this.contractType,
|
||||
required this.contractValue,
|
||||
required this.total,
|
||||
required this.addonsAmount,
|
||||
required this.dueAt,
|
||||
required this.workAmount,
|
||||
required this.items,
|
||||
required this.dispute});
|
||||
|
||||
Map<String, dynamic> toJson() => _$InvoiceModelToJson(this);
|
||||
|
||||
copyWith({
|
||||
String? id,
|
||||
InvoiceStatus? status,
|
||||
BusinessModel? business,
|
||||
EventModel? event,
|
||||
String? contractType,
|
||||
String? contractValue,
|
||||
String? dueAt,
|
||||
double? total,
|
||||
double? addonsAmount,
|
||||
double? workAmount,
|
||||
List<InvoiceItemModel>? items,
|
||||
InvoiceDeclineModel? dispute,
|
||||
}) {
|
||||
return InvoiceModel(
|
||||
id: id ?? this.id,
|
||||
status: status ?? this.status,
|
||||
business: business ?? this.business,
|
||||
event: event ?? this.event,
|
||||
contractType: contractType ?? this.contractType,
|
||||
contractValue: contractValue ?? this.contractValue,
|
||||
total: total ?? this.total,
|
||||
addonsAmount: addonsAmount ?? this.addonsAmount,
|
||||
workAmount: workAmount ?? this.workAmount,
|
||||
items: items ?? this.items,
|
||||
dueAt: dueAt ?? this.dueAt,
|
||||
dispute: dispute ?? this.dispute,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:krow/core/application/clients/api/api_exception.dart';
|
||||
import 'package:krow/core/application/di/injectable.dart';
|
||||
import 'package:krow/features/invoice/data/models/invoice_decline_model.dart';
|
||||
import 'package:krow/features/invoice/data/models/invoice_model.dart';
|
||||
import 'package:krow/features/invoice/domain/invoices_repository.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
part 'invoice_details_event.dart';
|
||||
part 'invoice_details_state.dart';
|
||||
|
||||
class InvoiceDetailsBloc
|
||||
extends Bloc<InvoiceDetailsEvent, InvoiceDetailsState> {
|
||||
InvoiceDetailsBloc(InvoiceModel invoice)
|
||||
: super(InvoiceDetailsState(invoiceModel: invoice)) {
|
||||
on<InvoiceApproveEvent>(_onInvoiceApproveEvent);
|
||||
on<InvoiceDisputeEvent>(_onInvoiceDisputeEvent);
|
||||
}
|
||||
|
||||
void _onInvoiceApproveEvent(
|
||||
InvoiceApproveEvent event, Emitter<InvoiceDetailsState> emit) async {
|
||||
emit(state.copyWith(inLoading: true));
|
||||
try {
|
||||
await getIt<InvoicesRepository>().approveInvoice(
|
||||
invoiceId: state.invoiceModel?.id ?? '',
|
||||
);
|
||||
emit(state.copyWith(inLoading: false, success: true));
|
||||
return;
|
||||
} catch (e) {
|
||||
if (e is DisplayableException) {
|
||||
emit(state.copyWith(inLoading: false, showErrorPopup: e.message));
|
||||
return;
|
||||
}
|
||||
}
|
||||
emit(state.copyWith(inLoading: false));
|
||||
}
|
||||
|
||||
void _onInvoiceDisputeEvent(
|
||||
InvoiceDisputeEvent event, Emitter<InvoiceDetailsState> emit) async {
|
||||
emit(state.copyWith(inLoading: true));
|
||||
try {
|
||||
await getIt<InvoicesRepository>().disputeInvoice(
|
||||
invoiceId: state.invoiceModel?.id ?? '',
|
||||
reason: event.reason,
|
||||
comment: event.comment,
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
inLoading: false,
|
||||
invoiceModel: state.invoiceModel?.copyWith(
|
||||
status: InvoiceStatus.disputed,
|
||||
dispute: InvoiceDeclineModel(
|
||||
id: '1',
|
||||
reason: event.reason,
|
||||
details: event.comment,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
} catch (e) {
|
||||
print(e);
|
||||
if (e is DisplayableException) {
|
||||
emit(state.copyWith(inLoading: false, showErrorPopup: e.message));
|
||||
return;
|
||||
}
|
||||
}
|
||||
emit(state.copyWith(inLoading: false));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
part of 'invoice_details_bloc.dart';
|
||||
|
||||
@immutable
|
||||
sealed class InvoiceDetailsEvent {}
|
||||
|
||||
class InvoiceApproveEvent extends InvoiceDetailsEvent {
|
||||
InvoiceApproveEvent();
|
||||
}
|
||||
|
||||
class InvoiceDisputeEvent extends InvoiceDetailsEvent {
|
||||
final String reason;
|
||||
final String comment;
|
||||
|
||||
InvoiceDisputeEvent({
|
||||
required this.reason,
|
||||
required this.comment,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
part of 'invoice_details_bloc.dart';
|
||||
|
||||
@immutable
|
||||
class InvoiceDetailsState {
|
||||
final String? showErrorPopup;
|
||||
final bool inLoading;
|
||||
final bool success;
|
||||
final InvoiceModel? invoiceModel;
|
||||
|
||||
const InvoiceDetailsState( {this.showErrorPopup, this.inLoading = false,this.invoiceModel, this.success = false});
|
||||
|
||||
InvoiceDetailsState copyWith({String? showErrorPopup, bool? inLoading, InvoiceModel? invoiceModel, bool? success}) {
|
||||
return InvoiceDetailsState(
|
||||
showErrorPopup: showErrorPopup ?? this.showErrorPopup,
|
||||
inLoading: inLoading ?? false,
|
||||
success: success ?? this.success,
|
||||
invoiceModel: invoiceModel ?? this.invoiceModel,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/core/application/di/injectable.dart';
|
||||
|
||||
import '../../invoice_entity.dart';
|
||||
import '../../invoices_repository.dart';
|
||||
import 'invoice_event.dart';
|
||||
import 'invoice_state.dart';
|
||||
|
||||
class InvoiceBloc extends Bloc<InvoiceEvent, InvoicesState> {
|
||||
var indexToStatus = <int, InvoiceStatusFilterType>{
|
||||
0: InvoiceStatusFilterType.all,
|
||||
1: InvoiceStatusFilterType.open,
|
||||
2: InvoiceStatusFilterType.disputed,
|
||||
3: InvoiceStatusFilterType.resolved,
|
||||
4: InvoiceStatusFilterType.paid,
|
||||
5: InvoiceStatusFilterType.overdue,
|
||||
6: InvoiceStatusFilterType.verified,
|
||||
};
|
||||
|
||||
InvoiceBloc()
|
||||
: super(const InvoicesState(tabs: {
|
||||
0: InvoiceTabState(items: [], inLoading: true),
|
||||
1: InvoiceTabState(items: []),
|
||||
2: InvoiceTabState(items: []),
|
||||
3: InvoiceTabState(items: []),
|
||||
4: InvoiceTabState(items: []),
|
||||
5: InvoiceTabState(items: []),
|
||||
6: InvoiceTabState(items: []),
|
||||
})) {
|
||||
on<InvoiceInitialEvent>(_onInitial);
|
||||
on<InvoiceTabChangedEvent>(_onTabChanged);
|
||||
on<LoadTabInvoiceEvent>(_onLoadTabItems);
|
||||
on<LoadMoreInvoiceEvent>(_onLoadMoreTabItems);
|
||||
|
||||
getIt<InvoicesRepository>().statusStream.listen((event) {
|
||||
add(LoadTabInvoiceEvent(status: event.index));
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _onInitial(InvoiceInitialEvent event, emit) async {
|
||||
add(const LoadTabInvoiceEvent(status: 0));
|
||||
}
|
||||
|
||||
Future<void> _onTabChanged(InvoiceTabChangedEvent event, emit) async {
|
||||
emit(state.copyWith(tabIndex: event.tabIndex));
|
||||
final currentTabState = state.tabs[event.tabIndex]!;
|
||||
if (currentTabState.items.isEmpty && !currentTabState.inLoading) {
|
||||
add(LoadTabInvoiceEvent(status: event.tabIndex));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onLoadTabItems(LoadTabInvoiceEvent event, emit) async {
|
||||
await _fetchInvoices(event.status, null, emit);
|
||||
}
|
||||
|
||||
Future<void> _onLoadMoreTabItems(LoadMoreInvoiceEvent event, emit) async {
|
||||
final currentTabState = state.tabs[event.status]!;
|
||||
if (!currentTabState.hasMoreItems || currentTabState.inLoading) return;
|
||||
await _fetchInvoices(event.status, currentTabState.items, emit);
|
||||
}
|
||||
|
||||
_fetchInvoices(
|
||||
int tabIndex, List<InvoiceListEntity>? previousItems, emit) async {
|
||||
if (previousItems != null && previousItems.lastOrNull?.cursor == null) {
|
||||
return;
|
||||
}
|
||||
final currentTabState = state.tabs[tabIndex]!;
|
||||
|
||||
emit(state.copyWith(
|
||||
tabs: {
|
||||
...state.tabs,
|
||||
tabIndex: currentTabState.copyWith(inLoading: true),
|
||||
},
|
||||
));
|
||||
|
||||
try {
|
||||
var items = await getIt<InvoicesRepository>().getInvoices(
|
||||
statusFilter: indexToStatus[tabIndex]!,
|
||||
lastItemId: previousItems?.lastOrNull?.cursor,
|
||||
);
|
||||
|
||||
// if(items.isNotEmpty){
|
||||
// items = List.generate(20, (i)=>items[0]);
|
||||
// }
|
||||
emit(state.copyWith(
|
||||
tabs: {
|
||||
...state.tabs,
|
||||
tabIndex: currentTabState.copyWith(
|
||||
items: (previousItems ?? [])..addAll(items),
|
||||
hasMoreItems: items.isNotEmpty,
|
||||
inLoading: false,
|
||||
),
|
||||
},
|
||||
));
|
||||
} catch (e, s) {
|
||||
debugPrint(e.toString());
|
||||
debugPrint(s.toString());
|
||||
emit(state.copyWith(
|
||||
tabs: {
|
||||
...state.tabs,
|
||||
tabIndex: currentTabState.copyWith(inLoading: false),
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
getIt<InvoicesRepository>().dispose();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
sealed class InvoiceEvent {
|
||||
const InvoiceEvent();
|
||||
}
|
||||
|
||||
class InvoiceInitialEvent extends InvoiceEvent {
|
||||
const InvoiceInitialEvent();
|
||||
}
|
||||
|
||||
class InvoiceTabChangedEvent extends InvoiceEvent {
|
||||
final int tabIndex;
|
||||
|
||||
const InvoiceTabChangedEvent({required this.tabIndex});
|
||||
}
|
||||
|
||||
class LoadTabInvoiceEvent extends InvoiceEvent {
|
||||
final int status;
|
||||
|
||||
const LoadTabInvoiceEvent({required this.status});
|
||||
}
|
||||
|
||||
class LoadMoreInvoiceEvent extends InvoiceEvent {
|
||||
final int status;
|
||||
|
||||
const LoadMoreInvoiceEvent({required this.status});
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import 'package:krow/features/invoice/domain/invoice_entity.dart';
|
||||
|
||||
class InvoicesState {
|
||||
final bool inLoading;
|
||||
final int tabIndex;
|
||||
|
||||
final Map<int, InvoiceTabState> tabs;
|
||||
|
||||
const InvoicesState(
|
||||
{this.inLoading = false, this.tabIndex = 0, required this.tabs});
|
||||
|
||||
InvoicesState copyWith({
|
||||
bool? inLoading,
|
||||
int? tabIndex,
|
||||
Map<int, InvoiceTabState>? tabs,
|
||||
}) {
|
||||
return InvoicesState(
|
||||
inLoading: inLoading ?? this.inLoading,
|
||||
tabIndex: tabIndex ?? this.tabIndex,
|
||||
tabs: tabs ?? this.tabs,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class InvoiceTabState {
|
||||
final List<InvoiceListEntity> items;
|
||||
final bool inLoading;
|
||||
final bool hasMoreItems;
|
||||
|
||||
const InvoiceTabState({
|
||||
required this.items,
|
||||
this.inLoading = false,
|
||||
this.hasMoreItems = true,
|
||||
});
|
||||
|
||||
InvoiceTabState copyWith({
|
||||
List<InvoiceListEntity>? items,
|
||||
bool? inLoading,
|
||||
bool? hasMoreItems,
|
||||
}) {
|
||||
return InvoiceTabState(
|
||||
items: items ?? this.items,
|
||||
inLoading: inLoading ?? this.inLoading,
|
||||
hasMoreItems: hasMoreItems ?? this.hasMoreItems,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import 'package:krow/features/invoice/data/models/invoice_model.dart';
|
||||
|
||||
class InvoiceListEntity {
|
||||
final String id;
|
||||
final InvoiceStatus status;
|
||||
String? cursor;
|
||||
final String invoiceNumber;
|
||||
final String eventName;
|
||||
final int count;
|
||||
final double value;
|
||||
final DateTime date;
|
||||
final DateTime dueAt;
|
||||
final String? poNumber;
|
||||
|
||||
final InvoiceModel? invoiceModel;
|
||||
|
||||
InvoiceListEntity({
|
||||
required this.id,
|
||||
required this.status,
|
||||
required this.invoiceNumber,
|
||||
required this.eventName,
|
||||
required this.count,
|
||||
required this.value,
|
||||
required this.date,
|
||||
required this.dueAt,
|
||||
this.poNumber,
|
||||
this.cursor,
|
||||
this.invoiceModel,
|
||||
});
|
||||
|
||||
InvoiceListEntity copyWith({
|
||||
String? id,
|
||||
String? cursor,
|
||||
InvoiceStatus? status,
|
||||
String? invoiceNumber,
|
||||
String? eventName,
|
||||
int? count,
|
||||
double? value,
|
||||
DateTime? date,
|
||||
DateTime? dueAt,
|
||||
String? poNumber,
|
||||
}) {
|
||||
return InvoiceListEntity(
|
||||
id: id ?? this.id,
|
||||
cursor: cursor ?? this.cursor,
|
||||
status: status ?? this.status,
|
||||
invoiceNumber: invoiceNumber ?? this.invoiceNumber,
|
||||
eventName: eventName ?? this.eventName,
|
||||
count: count ?? this.count,
|
||||
value: value ?? this.value,
|
||||
date: date ?? this.date,
|
||||
dueAt: date ?? this.dueAt,
|
||||
);
|
||||
}
|
||||
|
||||
InvoiceListEntity.fromModel(InvoiceModel model, {this.cursor})
|
||||
: id = model.id,
|
||||
poNumber = model.event?.purchaseOrder,
|
||||
status = model.status,
|
||||
invoiceNumber = model.event?.id ?? '',
|
||||
eventName = model.event!.name,
|
||||
count = model.items?.length?? 0,
|
||||
value = (model.total ?? 0.0),
|
||||
date = DateTime.parse(model.event?.date ?? ''),
|
||||
dueAt = DateTime.parse(model.dueAt ?? ''),
|
||||
invoiceModel = model;
|
||||
|
||||
InvoiceListEntity.empty()
|
||||
: id = 'INV-20250113-12345',
|
||||
status = InvoiceStatus.open,
|
||||
invoiceNumber = 'INV-20250113-12345',
|
||||
eventName = 'Event Name',
|
||||
count = 16,
|
||||
value = 1230,
|
||||
poNumber = 'PO-123456',
|
||||
date = DateTime.now(),
|
||||
dueAt = DateTime.now(),
|
||||
invoiceModel = null;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import 'package:krow/features/invoice/domain/invoice_entity.dart';
|
||||
|
||||
enum InvoiceStatusFilterType {
|
||||
all,
|
||||
open,
|
||||
disputed,
|
||||
resolved,
|
||||
paid,
|
||||
overdue,
|
||||
verified
|
||||
}
|
||||
|
||||
abstract class InvoicesRepository {
|
||||
Stream<dynamic> get statusStream;
|
||||
|
||||
Future<List<InvoiceListEntity>> getInvoices(
|
||||
{String? lastItemId, required InvoiceStatusFilterType statusFilter});
|
||||
|
||||
void dispose();
|
||||
|
||||
Future<void> approveInvoice({required String invoiceId});
|
||||
|
||||
Future<void> disputeInvoice({
|
||||
required String invoiceId,
|
||||
required String reason,
|
||||
required String comment,
|
||||
});
|
||||
}
|
||||
@@ -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/invoice/domain/blocs/invoices_list_bloc/invoice_bloc.dart';
|
||||
|
||||
import 'domain/blocs/invoices_list_bloc/invoice_event.dart';
|
||||
|
||||
@RoutePage()
|
||||
class InvoiceFlowScreen extends StatelessWidget {
|
||||
const InvoiceFlowScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => InvoiceBloc()..add(const InvoiceInitialEvent()),
|
||||
child: AutoRouter(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
|
||||
|
||||
class DisputeInfoDialog extends StatefulWidget {
|
||||
final String reason;
|
||||
final String details;
|
||||
final String? supportNote;
|
||||
|
||||
const DisputeInfoDialog(
|
||||
{super.key,
|
||||
required this.reason,
|
||||
required this.details,
|
||||
required this.supportNote});
|
||||
|
||||
static Future<Map<String, dynamic>?> showCustomDialog(BuildContext context,
|
||||
{required String reason,
|
||||
required String details,
|
||||
required String? supportNote}) async {
|
||||
return await showDialog<Map<String, dynamic>>(
|
||||
context: context,
|
||||
builder: (context) => DisputeInfoDialog(
|
||||
reason: reason, details: details, supportNote: supportNote),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<DisputeInfoDialog> createState() => _DisputeInfoDialogState();
|
||||
}
|
||||
|
||||
class _DisputeInfoDialogState extends State<DisputeInfoDialog> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Container(
|
||||
decoration: KwBoxDecorations.white24,
|
||||
margin: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Dispute Details',
|
||||
style: AppTextStyles.headingH3,
|
||||
)
|
||||
],
|
||||
),
|
||||
Gap(24),
|
||||
Text(
|
||||
'The reason why the invoice is being disputed:',
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
Gap(2),
|
||||
Text(
|
||||
widget.reason,
|
||||
style: AppTextStyles.bodyMediumMed
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
Divider(
|
||||
color: AppColors.grayTintStroke,
|
||||
height: 24,
|
||||
),
|
||||
Text('Status',
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.blackGray)),
|
||||
Gap(2),
|
||||
Text('Under Review',
|
||||
style: AppTextStyles.bodyMediumMed
|
||||
.copyWith(color: AppColors.primaryBlue)),
|
||||
Gap(12),
|
||||
Text(
|
||||
'Additional Details',
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
Gap(2),
|
||||
Text(
|
||||
widget.details,
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
),
|
||||
if (widget.supportNote?.isNotEmpty ?? false) ...[
|
||||
const Gap(12),
|
||||
Text(
|
||||
'Support Note',
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
Gap(2),
|
||||
Text(
|
||||
widget.supportNote ?? '',
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
),
|
||||
],
|
||||
const Gap(24),
|
||||
KwButton.primary(
|
||||
label: 'Back to Invoice',
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_input.dart';
|
||||
import 'package:krow/features/invoice/presentation/screens/invoice_details/dialogs/invoice_reason_dropdown.dart';
|
||||
|
||||
class ShiftDeclineDialog extends StatefulWidget {
|
||||
const ShiftDeclineDialog({super.key});
|
||||
|
||||
static Future<Map<String, dynamic>?> showCustomDialog(
|
||||
BuildContext context) async {
|
||||
return await showDialog<Map<String, dynamic>>(
|
||||
context: context,
|
||||
builder: (context) => const ShiftDeclineDialog(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<ShiftDeclineDialog> createState() => _ShiftDeclineDialogState();
|
||||
}
|
||||
|
||||
class _ShiftDeclineDialogState extends State<ShiftDeclineDialog> {
|
||||
String selectedReason = '';
|
||||
final _textEditingController = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: Center(
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
decoration: KwBoxDecorations.white24,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 56),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
height: 64,
|
||||
width: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.tintRed,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Center(
|
||||
child: Assets.images.icons.receiptSearch.svg(
|
||||
width: 32,
|
||||
height: 32,
|
||||
colorFilter: ColorFilter.mode(
|
||||
AppColors.statusError, BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
),
|
||||
Gap(32),
|
||||
Text(
|
||||
'Dispute Invoice',
|
||||
style: AppTextStyles.headingH1.copyWith(height: 1),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'If there’s an issue with this invoice, please select a reason below and provide any additional details. We’ll review your dispute and get back to you as soon as possible.',
|
||||
style: AppTextStyles.bodyMediumReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const Gap(8),
|
||||
InvoiceReasonDropdown(
|
||||
selectedReason: selectedReason,
|
||||
onReasonSelected: (String reason) {
|
||||
setState(() {
|
||||
selectedReason = reason;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
KwTextInput(
|
||||
controller: _textEditingController,
|
||||
minHeight: 144,
|
||||
maxLength: 300,
|
||||
showCounter: true,
|
||||
radius: 12,
|
||||
title: 'Additional reasons',
|
||||
hintText: 'Enter your main text here...',
|
||||
onChanged: (String value) {
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
const Gap(24),
|
||||
_buttonGroup(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buttonGroup(
|
||||
BuildContext context,
|
||||
) {
|
||||
return Column(
|
||||
children: [
|
||||
KwButton.primary(
|
||||
disabled: selectedReason.isEmpty || (_textEditingController.text.isEmpty),
|
||||
label: 'Submit Request',
|
||||
onPressed: () {
|
||||
context.pop({
|
||||
'reason': selectedReason,
|
||||
'comment': _textEditingController.text,
|
||||
});
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
KwButton.outlinedPrimary(
|
||||
label: 'Cancel',
|
||||
onPressed: () {
|
||||
context.pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_popup_menu.dart';
|
||||
|
||||
const reasons = [
|
||||
'Hours didn’t Match',
|
||||
'Calculation Issue',
|
||||
'Other (Please specify)',
|
||||
];
|
||||
|
||||
class InvoiceReasonDropdown extends StatelessWidget {
|
||||
final String? selectedReason;
|
||||
final Function(String reason) onReasonSelected;
|
||||
|
||||
const InvoiceReasonDropdown(
|
||||
{super.key,
|
||||
required this.selectedReason,
|
||||
required this.onReasonSelected});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: buildReasonInput(context, selectedReason),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> buildReasonInput(BuildContext context, String? selectedReason) {
|
||||
return [
|
||||
const Gap(24),
|
||||
Row(
|
||||
children: [
|
||||
const Gap(16),
|
||||
Text(
|
||||
'Select reason',
|
||||
style:
|
||||
AppTextStyles.bodyTinyReg.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(4),
|
||||
KwPopupMenu(
|
||||
horizontalPadding: 40,
|
||||
fit: KwPopupMenuFit.expand,
|
||||
customButtonBuilder: (context, isOpened) {
|
||||
return _buildMenuButton(isOpened, selectedReason);
|
||||
},
|
||||
menuItems: [
|
||||
...reasons
|
||||
.map((e) => _buildMenuItem(context, e, selectedReason ?? ''))
|
||||
])
|
||||
];
|
||||
}
|
||||
|
||||
Container _buildMenuButton(bool isOpened, String? selectedReason) {
|
||||
return Container(
|
||||
height: 48,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.grayWhite,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: isOpened ? AppColors.bgColorDark : AppColors.grayTintStroke,
|
||||
width: 1),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
selectedReason ?? 'Select reason from a list',
|
||||
style: AppTextStyles.bodyMediumReg.copyWith(
|
||||
color: selectedReason == null
|
||||
? AppColors.blackGray
|
||||
: AppColors.blackBlack),
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
KwPopupMenuItem _buildMenuItem(
|
||||
BuildContext context, String reason, String selectedReason) {
|
||||
return KwPopupMenuItem(
|
||||
title: reason,
|
||||
onTap: () {
|
||||
onReasonSelected(reason);
|
||||
},
|
||||
icon: Container(
|
||||
height: 16,
|
||||
width: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: selectedReason != reason ? null : AppColors.bgColorDark,
|
||||
shape: BoxShape.circle,
|
||||
border: selectedReason == reason
|
||||
? null
|
||||
: Border.all(color: AppColors.grayTintStroke, width: 1),
|
||||
),
|
||||
child: selectedReason == reason
|
||||
? Center(
|
||||
child: Assets.images.icons.receiptSearch.svg(
|
||||
height: 10,
|
||||
width: 10,
|
||||
colorFilter: const ColorFilter.mode(
|
||||
AppColors.grayWhite, BlendMode.srcIn),
|
||||
))
|
||||
: null,
|
||||
),
|
||||
textStyle: AppTextStyles.bodySmallMed,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:intl/intl.dart' show DateFormat;
|
||||
import 'package:krow/core/entity/staff_contact_entity.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/widgets/staff_position_details_widget.dart';
|
||||
|
||||
import '../../../../../../core/presentation/styles/theme.dart';
|
||||
|
||||
class StaffInvoiceContactInfoPopup {
|
||||
static Future<void> show(
|
||||
BuildContext context,
|
||||
StaffContact staff, {
|
||||
String? date,
|
||||
required double hours,
|
||||
required double subtotal,
|
||||
}) async {
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return Center(
|
||||
child: _StaffPopupWidget(
|
||||
staff,
|
||||
date: date,
|
||||
hours: hours,
|
||||
subtotal: subtotal,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _StaffPopupWidget extends StatelessWidget {
|
||||
final StaffContact staff;
|
||||
final String? date;
|
||||
|
||||
final double hours;
|
||||
|
||||
final double subtotal;
|
||||
|
||||
const _StaffPopupWidget(this.staff,
|
||||
{this.date, required this.hours, required this.subtotal});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
// margin: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: KwBoxDecorations.white24,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: _ongoingBtn(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _ongoingBtn(BuildContext context) {
|
||||
return [
|
||||
const Gap(32),
|
||||
StaffPositionAvatar(
|
||||
imageUrl: staff.photoUrl,
|
||||
userName: '${staff.firstName} ${staff.lastName}',
|
||||
status: staff.status,
|
||||
),
|
||||
const Gap(16),
|
||||
StaffContactsWidget(staff: staff),
|
||||
StaffPositionDetailsWidget(
|
||||
staff: staff, date: date, hours: hours, subtotal: subtotal),
|
||||
const Gap(12),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class StaffPositionDetailsWidget extends StatelessWidget {
|
||||
final StaffContact staff;
|
||||
final String? date;
|
||||
|
||||
final double hours;
|
||||
|
||||
final double subtotal;
|
||||
|
||||
const StaffPositionDetailsWidget({
|
||||
super.key,
|
||||
required this.staff,
|
||||
this.date,
|
||||
required this.hours,
|
||||
required this.subtotal,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
margin: const EdgeInsets.all(12),
|
||||
decoration: KwBoxDecorations.primaryLight8,
|
||||
child: Column(
|
||||
children: [
|
||||
_textRow('Date',
|
||||
DateFormat('MM.dd.yyyy').format(DateTime.parse(date ?? ''))),
|
||||
_textRow(
|
||||
'Start Time',
|
||||
DateFormat('hh:mm a').format(
|
||||
DateFormat('yyyy-MM-dd hh:mm:ss').parse(staff.startAt))),
|
||||
_textRow(
|
||||
'End Time',
|
||||
DateFormat('hh:mm a').format(
|
||||
DateFormat('yyyy-MM-dd hh:mm:ss').parse(staff.endAt))),
|
||||
if (staff.breakIn.isNotEmpty && staff.breakOut.isNotEmpty)
|
||||
_textRow(
|
||||
'Break',
|
||||
DateTime.parse(staff.breakIn)
|
||||
.difference(DateTime.parse(staff.breakOut))
|
||||
.inMinutes
|
||||
.toString() +
|
||||
' minutes'),
|
||||
_textRow('Rate (\$/h)', '\$${staff.rate.toStringAsFixed(2)}/h'),
|
||||
_textRow('Hours', '${hours.toStringAsFixed(2)}'),
|
||||
_textRow('Subtotal', '\$${subtotal.toStringAsFixed(2)}'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _textRow(String title, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: AppTextStyles.bodySmallReg.copyWith(
|
||||
color: AppColors.blackGray,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: AppTextStyles.bodySmallMed,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/scroll_layout_helper.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/dialogs/kw_dialog.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_app_bar.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
|
||||
import 'package:krow/features/invoice/data/models/invoice_decline_model.dart';
|
||||
import 'package:krow/features/invoice/data/models/invoice_model.dart';
|
||||
import 'package:krow/features/invoice/domain/blocs/invoice_details_bloc/invoice_details_bloc.dart';
|
||||
import 'package:krow/features/invoice/domain/invoice_entity.dart';
|
||||
import 'package:krow/features/invoice/presentation/screens/invoice_details/dialogs/dispute_info_dialog.dart';
|
||||
import 'package:krow/features/invoice/presentation/screens/invoice_details/dialogs/invoice_dispute_dialog.dart';
|
||||
import 'package:krow/features/invoice/presentation/screens/invoice_details/widgets/invoice_details_widget.dart';
|
||||
import 'package:krow/features/invoice/presentation/screens/invoice_details/widgets/invoice_from_to_widget.dart';
|
||||
import 'package:krow/features/invoice/presentation/screens/invoice_details/widgets/invoice_info_card.dart';
|
||||
import 'package:krow/features/invoice/presentation/screens/invoice_details/widgets/invoice_total_widget.dart';
|
||||
import 'package:modal_progress_hud_nsn/modal_progress_hud_nsn.dart';
|
||||
|
||||
@RoutePage()
|
||||
class InvoiceDetailsScreen extends StatelessWidget implements AutoRouteWrapper {
|
||||
final InvoiceListEntity invoice;
|
||||
|
||||
const InvoiceDetailsScreen({required this.invoice, super.key});
|
||||
|
||||
@override
|
||||
Widget wrappedRoute(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => InvoiceDetailsBloc(invoice.invoiceModel!),
|
||||
child: this);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: true,
|
||||
appBar: KwAppBar(
|
||||
titleText: 'Invoice Details',
|
||||
),
|
||||
body: BlocConsumer<InvoiceDetailsBloc, InvoiceDetailsState>(
|
||||
listener: (context, state) {
|
||||
if (state.showErrorPopup != null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(state.showErrorPopup ?? ''),
|
||||
));
|
||||
} else if (state.success) {
|
||||
context.router.maybePop();
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return ModalProgressHUD(
|
||||
inAsyncCall: state.inLoading,
|
||||
child: ScrollLayoutHelper(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 24),
|
||||
upperWidget: Column(
|
||||
children: [
|
||||
InvoiceInfoCardWidget(item: state.invoiceModel??invoice.invoiceModel!),
|
||||
Gap(12),
|
||||
InvoiceFromToWidget(item: state.invoiceModel??invoice.invoiceModel!),
|
||||
Gap(12),
|
||||
InvoiceDetailsWidget(invoice: state.invoiceModel??invoice.invoiceModel!),
|
||||
Gap(12),
|
||||
InvoiceTotalWidget(),
|
||||
Gap(24),
|
||||
],
|
||||
),
|
||||
lowerWidget: _buildButtons(context, state),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_buildButtons(BuildContext context, InvoiceDetailsState state) {
|
||||
return Column(
|
||||
children: [
|
||||
if (invoice.invoiceModel?.status == InvoiceStatus.open) ...[
|
||||
KwButton.primary(
|
||||
label: 'Approve Invoice',
|
||||
onPressed: () {
|
||||
context.read<InvoiceDetailsBloc>().add(InvoiceApproveEvent());
|
||||
},
|
||||
),
|
||||
Gap(8),
|
||||
KwButton.outlinedPrimary(
|
||||
label: 'Dispute Invoice',
|
||||
onPressed: () async {
|
||||
var result = await ShiftDeclineDialog.showCustomDialog(context);
|
||||
print(result);
|
||||
if (result != null) {
|
||||
context.read<InvoiceDetailsBloc>().add(InvoiceDisputeEvent(
|
||||
reason: result['reason'], comment: result['comment']));
|
||||
KwDialog.show(
|
||||
icon: Assets.images.icons.receiptSearch,
|
||||
state: KwDialogState.negative,
|
||||
context: context,
|
||||
title: 'Request is \nUnder Review',
|
||||
message:
|
||||
'Thank you! Your request for invoice issue, it is now under review. You will be notified of the outcome shortly.',
|
||||
primaryButtonLabel: 'Back to Event',
|
||||
onPrimaryButtonPressed: (dialogContext) {
|
||||
dialogContext.pop();
|
||||
},
|
||||
secondaryButtonLabel: 'Cancel',
|
||||
onSecondaryButtonPressed: (dialogContext) {
|
||||
dialogContext.pop();
|
||||
context.router.maybePop();
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
).copyWith(
|
||||
color: AppColors.statusError, borderColor: AppColors.statusError),
|
||||
],
|
||||
if (invoice.invoiceModel?.status == InvoiceStatus.disputed) ...[
|
||||
_buildViewDispute(context, state.invoiceModel?.dispute),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
_buildViewDispute(BuildContext context, InvoiceDeclineModel? dispute) {
|
||||
return KwButton.primary(
|
||||
label: 'View Dispute',
|
||||
onPressed: () {
|
||||
DisputeInfoDialog.showCustomDialog(
|
||||
context,
|
||||
reason: dispute?.reason ?? '',
|
||||
details: dispute?.details ?? '',
|
||||
supportNote: dispute?.supportNote,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:expandable/expandable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/data/models/shift/event_shift_position_model.dart';
|
||||
import 'package:krow/core/entity/staff_contact_entity.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/features/invoice/data/models/invoice_item_model.dart';
|
||||
import 'package:krow/features/invoice/data/models/invoice_model.dart';
|
||||
|
||||
import '../../../../../../core/data/models/staff/pivot.dart';
|
||||
import '../../../../../../core/entity/position_entity.dart';
|
||||
import '../dialogs/staff_contact_info_popup.dart';
|
||||
|
||||
class InvoiceDetailsWidget extends StatefulWidget {
|
||||
final InvoiceModel invoice;
|
||||
|
||||
const InvoiceDetailsWidget({super.key, required this.invoice});
|
||||
|
||||
@override
|
||||
State<InvoiceDetailsWidget> createState() => _InvoiceDetailsWidgetState();
|
||||
}
|
||||
|
||||
class _InvoiceDetailsWidgetState extends State<InvoiceDetailsWidget> {
|
||||
List<ExpandableController> controllers = [];
|
||||
|
||||
ExpandableController showMoreController = ExpandableController();
|
||||
|
||||
Map<EventShiftPositionModel, List<InvoiceItemModel>> staffByPosition = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
staffByPosition = groupByPosition(widget.invoice.items ?? []);
|
||||
controllers =
|
||||
List.generate(staffByPosition.length, (_) => ExpandableController());
|
||||
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Invoice details',
|
||||
style: AppTextStyles.headingH3,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
decoration: KwBoxDecorations.white12,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
buildTable(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildTable() {
|
||||
final columnWidths = {
|
||||
0: const FlexColumnWidth(),
|
||||
1: const IntrinsicColumnWidth(),
|
||||
2: const IntrinsicColumnWidth(),
|
||||
3: const IntrinsicColumnWidth(),
|
||||
};
|
||||
|
||||
final rows = <TableRow>[];
|
||||
|
||||
_buildTableHeader(rows);
|
||||
|
||||
var maxVisibleRows = staffByPosition.length;
|
||||
|
||||
for (int index = 0; index < maxVisibleRows; index++) {
|
||||
final position = staffByPosition.keys.elementAt(index);
|
||||
final group = staffByPosition[position]!;
|
||||
final controller = controllers[index];
|
||||
|
||||
final roleDuration =
|
||||
group.fold(0.0, (sum, item) => sum + (item.workHours ?? 0));
|
||||
final subTotal =
|
||||
group.fold(0.0, (sum, item) => sum + (item.totalAmount ?? 0));
|
||||
|
||||
_buildRoleRows(rows, controller, position, roleDuration, subTotal,
|
||||
isLastRow: index == maxVisibleRows - 1);
|
||||
|
||||
_buildStaffsRows(controller, group, rows, position);
|
||||
}
|
||||
|
||||
return AnimatedSize(
|
||||
alignment: Alignment.topCenter,
|
||||
duration: Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOut,
|
||||
child: Table(
|
||||
columnWidths: columnWidths,
|
||||
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
|
||||
children: rows,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _buildTableHeader(List<TableRow> rows) {
|
||||
return rows.add(
|
||||
TableRow(
|
||||
children: buildRowCells(
|
||||
[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 22, top: 12, bottom: 12),
|
||||
child: Text('Role', style: AppTextStyles.bodyMediumMed),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 8, right: 16, top: 12, bottom: 12),
|
||||
child: Text('Rate (\$/h)', style: AppTextStyles.bodyMediumMed),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 8, right: 16, top: 12, bottom: 12),
|
||||
child: Text('Hours', style: AppTextStyles.bodyMediumMed),
|
||||
),
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(left: 8, right: 8, top: 12, bottom: 12),
|
||||
child: Text('Subtotal', style: AppTextStyles.bodyMediumMed),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _buildRoleRows(List<TableRow> rows, ExpandableController controller,
|
||||
EventShiftPositionModel position, double roleDuration, double subTotal,
|
||||
{required bool isLastRow}) {
|
||||
return rows.add(
|
||||
TableRow(
|
||||
children: buildRowCells(
|
||||
isLastRow: isLastRow && !controller.expanded,
|
||||
[
|
||||
expandableCell(
|
||||
controller: controller,
|
||||
padding: const EdgeInsets.only(left: 6, top: 16, bottom: 13),
|
||||
child: Row(
|
||||
children: [
|
||||
AnimatedRotation(
|
||||
duration: Duration(milliseconds: 300),
|
||||
turns: controller.expanded ? -0.5 : 0,
|
||||
child: Assets.images.icons.chevronDown
|
||||
.svg(width: 12, height: 12),
|
||||
),
|
||||
Gap(4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
position.businessSkill.skill?.name ?? '',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
expandableCell(
|
||||
controller: controller,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12, right: 8, top: 16, bottom: 16),
|
||||
child: Text(
|
||||
'\$${position.rate.toStringAsFixed(2)}/h',
|
||||
style: AppTextStyles.bodyMediumReg,
|
||||
),
|
||||
),
|
||||
expandableCell(
|
||||
controller: controller,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12, right: 8, top: 16, bottom: 16),
|
||||
child: Text(roleDuration.toStringAsFixed(1),
|
||||
style: AppTextStyles.bodyMediumReg),
|
||||
),
|
||||
expandableCell(
|
||||
controller: controller,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12, right: 8, top: 16, bottom: 16),
|
||||
child: Text('\$${subTotal.toStringAsFixed(2)}',
|
||||
style: AppTextStyles.bodyMediumReg),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _buildStaffsRows(
|
||||
ExpandableController controller,
|
||||
List<InvoiceItemModel> group,
|
||||
List<TableRow> rows,
|
||||
EventShiftPositionModel position) {
|
||||
if (controller.expanded) {
|
||||
for (int i = 0; i < group.length; i++) {
|
||||
final user = group[i];
|
||||
rows.add(
|
||||
TableRow(
|
||||
children: buildRowCells(isLastRow: i == group.length - 1, [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
StaffInvoiceContactInfoPopup.show(
|
||||
context,
|
||||
makeStaffContact(
|
||||
user,
|
||||
position,
|
||||
),
|
||||
date: widget.invoice.event?.date,
|
||||
hours: user.workHours ?? 0.0,
|
||||
subtotal: user.totalAmount ?? 0.0);
|
||||
},
|
||||
child: buildAnimatedStaffCell(
|
||||
visible: controller.expanded,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 12.0),
|
||||
child: Text(
|
||||
'${user.staff?.firstName ?? ''} ${user.staff?.lastName ?? ''}',
|
||||
style: AppTextStyles.bodyTinyReg.copyWith(
|
||||
color: AppColors.blackGray,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
buildAnimatedStaffCell(
|
||||
visible: controller.expanded,
|
||||
child: Text('\$${(user.rate?.toStringAsFixed(2)) ?? 0}/h',
|
||||
style: AppTextStyles.bodyTinyReg),
|
||||
),
|
||||
buildAnimatedStaffCell(
|
||||
visible: controller.expanded,
|
||||
child: Text('${user.workHours?.toStringAsFixed(1) ?? 0}',
|
||||
style: AppTextStyles.bodyTinyReg),
|
||||
),
|
||||
buildAnimatedStaffCell(
|
||||
visible: controller.expanded,
|
||||
child: Text('\$${user.totalAmount ?? 0}',
|
||||
style: AppTextStyles.bodyTinyReg),
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> buildRowCells(List<Widget> cells, {bool isLastRow = false}) {
|
||||
return List.generate(cells.length, (i) {
|
||||
final isLastColumn = i == cells.length - 1;
|
||||
return buildGridCell(
|
||||
child: cells[i],
|
||||
showRight: !isLastColumn,
|
||||
showBottom: !isLastRow,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget expandableCell(
|
||||
{required Widget child,
|
||||
required ExpandableController controller,
|
||||
required EdgeInsets padding}) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
controller.expanded = !controller.expanded;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
color: Colors.transparent, padding: padding, child: child));
|
||||
}
|
||||
|
||||
Widget buildGridCell({
|
||||
required Widget child,
|
||||
bool showRight = true,
|
||||
bool showBottom = true,
|
||||
EdgeInsets padding = const EdgeInsets.all(8),
|
||||
}) {
|
||||
return Container(
|
||||
padding: padding,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
right: showRight
|
||||
? BorderSide(color: Colors.grey.shade300, width: 1)
|
||||
: BorderSide.none,
|
||||
bottom: showBottom
|
||||
? BorderSide(color: Colors.grey.shade300, width: 1)
|
||||
: BorderSide.none,
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildAnimatedStaffCell({
|
||||
required bool visible,
|
||||
required Widget child,
|
||||
Duration duration = const Duration(milliseconds: 300),
|
||||
Curve curve = Curves.easeInOut,
|
||||
}) {
|
||||
return AnimatedContainer(
|
||||
duration: duration,
|
||||
curve: curve,
|
||||
color: Colors.transparent,
|
||||
height: visible ? null : 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 10, right: 6, top: 10, bottom: 10),
|
||||
child: AnimatedOpacity(
|
||||
duration: duration,
|
||||
opacity: visible ? 1 : 0,
|
||||
child: visible ? child : IgnorePointer(child: child),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Map<EventShiftPositionModel, List<InvoiceItemModel>> groupByPosition(
|
||||
List<InvoiceItemModel> items,
|
||||
) {
|
||||
|
||||
final Map<String, EventShiftPositionModel> positionCache = {};
|
||||
final Map<EventShiftPositionModel, List<InvoiceItemModel>> grouped = {};
|
||||
|
||||
for (final item in items) {
|
||||
final position = item.position;
|
||||
|
||||
if (position == null) continue;
|
||||
|
||||
final id = position.businessSkill.id ?? '';
|
||||
|
||||
final existingPosition = positionCache[id];
|
||||
|
||||
final key = existingPosition ??
|
||||
() {
|
||||
positionCache[id] = position;
|
||||
return position;
|
||||
}();
|
||||
|
||||
grouped.putIfAbsent(key, () => []);
|
||||
grouped[key]!.add(item);
|
||||
}
|
||||
return grouped;
|
||||
}
|
||||
|
||||
StaffContact makeStaffContact(
|
||||
InvoiceItemModel user, EventShiftPositionModel position) {
|
||||
var staff = user.staff!;
|
||||
var pivot = user.position!.staff!
|
||||
.firstWhere(
|
||||
(element) => element.id == user.staff?.id,
|
||||
)
|
||||
.pivot;
|
||||
|
||||
return StaffContact(
|
||||
id: staff.id ?? '',
|
||||
photoUrl: staff.avatar ?? '',
|
||||
firstName: staff.firstName ?? '',
|
||||
lastName: staff.lastName ?? '',
|
||||
phoneNumber: staff.phone ?? '',
|
||||
email: staff.email ?? '',
|
||||
rate: position.businessSkill.price ?? 0,
|
||||
status: pivot?.status ?? PivotStatus.assigned,
|
||||
startAt: pivot?.clockIn ?? '',
|
||||
endAt: pivot?.clockOut ?? '',
|
||||
breakIn: pivot?.breakIn ?? '',
|
||||
breakOut: pivot?.breakOut ?? '',
|
||||
parentPosition: PositionEntity.fromDto(
|
||||
position,
|
||||
DateTime.tryParse(widget.invoice.event?.date ?? '') ??
|
||||
DateTime.now()),
|
||||
skillName: position.businessSkill.skill?.name ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import 'package:expandable/expandable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/features/invoice/data/models/invoice_model.dart';
|
||||
import 'package:krow/features/invoice/domain/blocs/invoice_details_bloc/invoice_details_bloc.dart';
|
||||
import 'package:krow/features/invoice/domain/invoice_entity.dart';
|
||||
|
||||
class InvoiceFromToWidget extends StatefulWidget {
|
||||
final InvoiceModel item;
|
||||
|
||||
const InvoiceFromToWidget({super.key, required this.item});
|
||||
|
||||
@override
|
||||
State<InvoiceFromToWidget> createState() => _InvoiceFromToWidgetState();
|
||||
}
|
||||
|
||||
class _InvoiceFromToWidgetState extends State<InvoiceFromToWidget> {
|
||||
ExpandableController? _expandableFromController;
|
||||
ExpandableController? _expandableToController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_expandableFromController = ExpandableController(initialExpanded: true);
|
||||
_expandableToController = ExpandableController(initialExpanded: true);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_expandableFromController?.dispose();
|
||||
_expandableToController?.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<InvoiceDetailsBloc, InvoiceDetailsState>(
|
||||
builder: (context, state) {
|
||||
return ExpandableTheme(
|
||||
data: ExpandableThemeData(
|
||||
headerAlignment: ExpandablePanelHeaderAlignment.center,
|
||||
iconPadding: const EdgeInsets.only(left: 12),
|
||||
),
|
||||
child: Container(
|
||||
decoration: KwBoxDecorations.white12,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Gap(12),
|
||||
ExpandablePanel(
|
||||
collapsed: Container(),
|
||||
controller: _expandableFromController,
|
||||
expanded: _fromInfo(),
|
||||
header: _buildHeader(
|
||||
context, _expandableFromController, 'From: Legendary'),
|
||||
),
|
||||
ExpandablePanel(
|
||||
collapsed: Container(),
|
||||
controller: _expandableToController,
|
||||
expanded: _toInfo(state.invoiceModel!),
|
||||
header: _buildHeader(
|
||||
context, _expandableToController, 'To: ${state.invoiceModel?.business?.name}'),
|
||||
),
|
||||
Gap(12),
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context, ExpandableController? controller,
|
||||
String title,) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
controller?.toggle();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
color: Colors.transparent,
|
||||
child: Text(
|
||||
title,
|
||||
style: AppTextStyles.headingH3,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _fromInfo() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Gap(4),
|
||||
infoElement('Address', '848 E Gish Rd, Suite 1 San Jose, CA 95112'),
|
||||
Gap(12),
|
||||
infoElement('Phone Number', '4088360180'),
|
||||
Gap(12),
|
||||
infoElement('Email', 'orders@legendaryeventstaff.com'),
|
||||
Gap(20),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _toInfo(InvoiceModel invoice) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Gap(4),
|
||||
infoElement('Hub', invoice.event?.hub?.name ?? ''),
|
||||
Gap(12),
|
||||
infoElement('Address', invoice.business?.registration ?? ''),
|
||||
Gap(12),
|
||||
infoElement('Full Name', '${invoice.business?.contact?.firstName??''} ${invoice.business?.contact?.lastName??''}' ),
|
||||
Gap(12),
|
||||
infoElement('Phone Number', invoice.business?.contact?.phone ?? ''),
|
||||
Gap(12),
|
||||
infoElement('Email', invoice.business?.contact?.email ?? ''),
|
||||
Gap(12),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget infoElement(String title, String value) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style:
|
||||
AppTextStyles.bodySmallReg.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
Gap(2),
|
||||
Text(
|
||||
value,
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow/core/application/common/str_extensions.dart';
|
||||
import 'package:krow/core/application/routing/routes.gr.dart';
|
||||
import 'package:krow/core/entity/event_entity.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/icon_row_info_widget.dart';
|
||||
import 'package:krow/features/invoice/data/models/invoice_model.dart';
|
||||
import 'package:krow/features/invoice/domain/invoice_entity.dart';
|
||||
|
||||
class InvoiceInfoCardWidget extends StatelessWidget {
|
||||
final InvoiceModel item;
|
||||
|
||||
const InvoiceInfoCardWidget({
|
||||
required this.item,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: KwBoxDecorations.white12,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildStatusRow(),
|
||||
const Gap(24),
|
||||
Text('INV-${item.id}', style: AppTextStyles.headingH1),
|
||||
Gap(4),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if(item.event == null) return;
|
||||
context.router.push(EventDetailsRoute(event: EventEntity.fromEventDto(item.event!)));
|
||||
},
|
||||
child: Text(item.event!.name,
|
||||
style: AppTextStyles.bodyMediumReg
|
||||
.copyWith(color: AppColors.blackGray,decoration: TextDecoration.underline)),
|
||||
),
|
||||
const Gap(24),
|
||||
IconRowInfoWidget(
|
||||
icon: Assets.images.icons.calendar.svg(),
|
||||
title: 'Date',
|
||||
value: DateFormat('MM.dd.yyyy').format(DateTime.parse(item.event?.date ?? '')),
|
||||
),
|
||||
const Gap(24),
|
||||
IconRowInfoWidget(
|
||||
icon: Assets.images.icons.calendar.svg(),
|
||||
title: 'Due Date',
|
||||
value: DateFormat('MM.dd.yyyy').format(DateTime.parse(item.dueAt ?? '')),
|
||||
),
|
||||
if (item.event?.purchaseOrder != null) ...[
|
||||
const Gap(24),
|
||||
IconRowInfoWidget(
|
||||
icon: Assets.images.icons.calendar.svg(),
|
||||
title: 'PO Number',
|
||||
value: item.event!.purchaseOrder!,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Row _buildStatusRow() {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
height: 48,
|
||||
width: 48,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.tintGreen,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Center(
|
||||
child: Assets.images.icons.moneySend.svg(
|
||||
colorFilter: const ColorFilter.mode(
|
||||
AppColors.statusSuccess, BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 28,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: item.status.color,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2.0),
|
||||
child: Text(
|
||||
item.status.name.capitalize(),
|
||||
style: AppTextStyles.bodySmallMed
|
||||
.copyWith(color: AppColors.grayWhite),
|
||||
),
|
||||
),
|
||||
))
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension on InvoiceStatus {
|
||||
Color get color {
|
||||
switch (this) {
|
||||
case InvoiceStatus.open:
|
||||
return AppColors.primaryBlue;
|
||||
case InvoiceStatus.disputed:
|
||||
return AppColors.statusWarning;
|
||||
case InvoiceStatus.resolved:
|
||||
return AppColors.statusSuccess;
|
||||
case InvoiceStatus.paid:
|
||||
return AppColors.blackGray;
|
||||
case InvoiceStatus.overdue:
|
||||
return AppColors.statusError;
|
||||
case InvoiceStatus.verified:
|
||||
return AppColors.bgColorDark;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/features/invoice/data/models/invoice_model.dart';
|
||||
import 'package:krow/features/invoice/domain/blocs/invoice_details_bloc/invoice_details_bloc.dart';
|
||||
import 'package:krow/features/invoice/domain/invoice_entity.dart';
|
||||
|
||||
class InvoiceTotalWidget extends StatelessWidget {
|
||||
|
||||
const InvoiceTotalWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<InvoiceDetailsBloc, InvoiceDetailsState>(
|
||||
builder: (context, state) {
|
||||
return Container(
|
||||
decoration: KwBoxDecorations.white12,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Total', style: AppTextStyles.headingH3),
|
||||
Gap(24),
|
||||
Container(
|
||||
decoration: KwBoxDecorations.primaryLight8,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
totalInfoRow('Sub Total', '\$${state.invoiceModel?.workAmount?.toStringAsFixed(2)??'0'}'),
|
||||
Gap(8),
|
||||
totalInfoRow('Other Charges', '\$${state.invoiceModel?.addonsAmount?.toStringAsFixed(2)??'0'}'),
|
||||
Gap(8),
|
||||
totalInfoRow('Grand Total', '\$${state.invoiceModel?.total?.toStringAsFixed(2)??'0'}'),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget totalInfoRow(String title, String value) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(title,
|
||||
style: AppTextStyles.bodyMediumReg
|
||||
.copyWith(color: AppColors.blackGray)),
|
||||
Gap(20),
|
||||
Text(value, style: AppTextStyles.bodyMediumMed)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user