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