feat: Refactor code structure and optimize performance across multiple modules
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@RoutePage()
|
||||
class CreateEventFlowScreen extends StatelessWidget {
|
||||
const CreateEventFlowScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const AutoRouter();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import 'package:graphql/client.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:krow/core/application/clients/api/api_client.dart';
|
||||
import 'package:krow/core/data/models/event/addon_model.dart';
|
||||
import 'package:krow/core/data/models/event/business_member_model.dart';
|
||||
import 'package:krow/core/data/models/event/hub_model.dart';
|
||||
import 'package:krow/core/data/models/event/tag_model.dart';
|
||||
import 'package:krow/core/data/models/shift/business_skill_model.dart';
|
||||
import 'package:krow/core/data/models/shift/department_model.dart';
|
||||
import 'package:krow/features/create_event/data/create_event_gql.dart';
|
||||
|
||||
@Injectable()
|
||||
class CreateEventApiProvider {
|
||||
final ApiClient _client;
|
||||
|
||||
CreateEventApiProvider({required ApiClient client}) : _client = client;
|
||||
|
||||
Future<List<HubModel>> getHubs() async {
|
||||
QueryResult result = await _client.query(
|
||||
schema: getClientHubsQuery,
|
||||
);
|
||||
if (result.hasException) {
|
||||
throw Exception(result.exception.toString());
|
||||
}
|
||||
|
||||
if (result.data == null || result.data!['client_hubs'] == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (result.data!['client_hubs'] as List)
|
||||
.map((e) => HubModel.fromJson(e))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<List<AddonModel>> getAddons() async {
|
||||
QueryResult result = await _client.query(
|
||||
schema: getClientAddonsQuery,
|
||||
);
|
||||
if (result.hasException) {
|
||||
throw Exception(result.exception.toString());
|
||||
}
|
||||
|
||||
if (result.data == null || result.data!['client_business_addons'] == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (result.data!['client_business_addons'] as List)
|
||||
.map((e) => AddonModel.fromJson(e))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<List<TagModel>> getTags() async {
|
||||
QueryResult result = await _client.query(
|
||||
schema: getClientTagsQuery,
|
||||
);
|
||||
if (result.hasException) {
|
||||
throw Exception(result.exception.toString());
|
||||
}
|
||||
|
||||
if (result.data == null || result.data!['client_tags'] == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (result.data!['client_tags'] as List)
|
||||
.map((e) => TagModel.fromJson(e))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<List<BusinessMemberModel>> getContacts() async {
|
||||
QueryResult result = await _client.query(
|
||||
schema: getClientMembersQuery,
|
||||
);
|
||||
if (result.hasException) {
|
||||
throw Exception(result.exception.toString());
|
||||
}
|
||||
|
||||
if (result.data == null || result.data!['client_shift_contacts'] == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (result.data!['client_shift_contacts'] as List)
|
||||
.map((e) => BusinessMemberModel.fromJson(e))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<List<BusinessSkillModel>> getSkill() async {
|
||||
QueryResult result = await _client.query(
|
||||
schema: getClientBusinessSkillQuery,
|
||||
);
|
||||
if (result.hasException) {
|
||||
throw Exception(result.exception.toString());
|
||||
}
|
||||
|
||||
if (result.data == null || result.data!['client_business_skills'] == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (result.data!['client_business_skills'] as List)
|
||||
.map((e) => BusinessSkillModel.fromJson(e))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<List<DepartmentModel>> getDepartments(String hubId) async {
|
||||
QueryResult result = await _client.query(
|
||||
schema: getClientDepartmentsQuery,
|
||||
body: {'hub_id': hubId},
|
||||
);
|
||||
if (result.hasException) {
|
||||
throw Exception(result.exception.toString());
|
||||
}
|
||||
if (result.data == null || result.data!['client_hub_departments'] == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (result.data!['client_hub_departments'] as List)
|
||||
.map((e) => DepartmentModel.fromJson(e))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
const String getClientHubsQuery = r'''
|
||||
query getClientHubs() {
|
||||
client_hubs {
|
||||
id
|
||||
name
|
||||
address
|
||||
full_address {
|
||||
street_number
|
||||
zip_code
|
||||
latitude
|
||||
longitude
|
||||
formatted_address
|
||||
street
|
||||
region
|
||||
city
|
||||
country
|
||||
}
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String getClientAddonsQuery = r'''
|
||||
query getClientAddons() {
|
||||
client_business_addons {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String getClientTagsQuery = r'''
|
||||
query getClientTags() {
|
||||
client_tags {
|
||||
id
|
||||
name
|
||||
slug
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String getClientBusinessSkillQuery = r'''
|
||||
query getClientSkills() {
|
||||
client_business_skills {
|
||||
id
|
||||
skill {
|
||||
id
|
||||
name
|
||||
slug
|
||||
price
|
||||
}
|
||||
price
|
||||
is_active
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String getClientMembersQuery = r'''
|
||||
query getClientMembers() {
|
||||
client_shift_contacts() {
|
||||
id
|
||||
first_name
|
||||
last_name
|
||||
title
|
||||
avatar
|
||||
auth_info {
|
||||
email
|
||||
phone
|
||||
}
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String getClientDepartmentsQuery = r'''
|
||||
query getClientSkills($hub_id: ID!) {
|
||||
client_hub_departments(hub_id: $hub_id) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:krow/core/data/models/event/addon_model.dart';
|
||||
import 'package:krow/core/data/models/event/business_member_model.dart';
|
||||
import 'package:krow/core/data/models/event/hub_model.dart';
|
||||
import 'package:krow/core/data/models/event/tag_model.dart';
|
||||
import 'package:krow/core/data/models/shift/business_skill_model.dart';
|
||||
import 'package:krow/core/data/models/shift/department_model.dart';
|
||||
import 'package:krow/features/create_event/data/create_event_api_provider.dart';
|
||||
import 'package:krow/features/create_event/domain/create_event_repository.dart';
|
||||
|
||||
@Singleton(as: CreateEventRepository)
|
||||
class CreateEventRepositoryImpl extends CreateEventRepository {
|
||||
final CreateEventApiProvider _apiProvider;
|
||||
|
||||
CreateEventRepositoryImpl({required CreateEventApiProvider apiProvider})
|
||||
: _apiProvider = apiProvider;
|
||||
|
||||
@override
|
||||
Future<List<HubModel>> getHubs() async {
|
||||
return _apiProvider.getHubs();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<AddonModel>> getAddons() async {
|
||||
return _apiProvider.getAddons();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<TagModel>> getTags() async {
|
||||
return _apiProvider.getTags();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<BusinessMemberModel>> getContacts() async {
|
||||
return _apiProvider.getContacts();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<BusinessSkillModel>> getSkills() {
|
||||
return _apiProvider.getSkill();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<DepartmentModel>> getDepartments(String hubId) {
|
||||
return _apiProvider.getDepartments(hubId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/core/application/di/injectable.dart';
|
||||
import 'package:krow/core/data/models/event/addon_model.dart';
|
||||
import 'package:krow/core/data/models/event/business_member_model.dart';
|
||||
import 'package:krow/core/data/models/event/event_model.dart';
|
||||
import 'package:krow/core/data/models/event/hub_model.dart';
|
||||
import 'package:krow/core/data/models/event/tag_model.dart';
|
||||
import 'package:krow/core/data/models/shift/business_skill_model.dart';
|
||||
import 'package:krow/core/data/models/shift/department_model.dart';
|
||||
import 'package:krow/core/entity/event_entity.dart';
|
||||
import 'package:krow/core/entity/shift_entity.dart';
|
||||
import 'package:krow/core/sevices/create_event_service/create_event_service.dart';
|
||||
import 'package:krow/features/create_event/domain/create_event_repository.dart';
|
||||
import 'package:krow/features/create_event/domain/input_validator.dart';
|
||||
import 'package:krow/features/create_event/presentation/create_shift_details_section/bloc/create_shift_details_bloc.dart';
|
||||
|
||||
part 'create_event_event.dart';
|
||||
part 'create_event_state.dart';
|
||||
|
||||
class CreateEventBloc extends Bloc<CreateEventEvent, CreateEventState> {
|
||||
CreateEventBloc() : super(CreateEventState(entity: EventEntity.empty())) {
|
||||
on<CreateEventInit>(_onInit);
|
||||
// on<CreateEventChangeContractType>(_onChangeContractType);
|
||||
on<CreateEventChangeHub>(_onChangeHub);
|
||||
// on<CreateEventChangeContractNumber>(_onChangeContractNumber);
|
||||
on<CreateEventChangePoNumber>(_onChangePoNumber);
|
||||
on<CreateEventTagSelected>(_onTagSelected);
|
||||
on<CreateEventAddShift>(_onAddShift);
|
||||
on<CreateEventRemoveShift>(_onRemoveShift);
|
||||
on<CreateEventAddInfoChange>(_onAddInfoChange);
|
||||
on<CreateEventNameChange>(_onNameChange);
|
||||
on<CreateEventToggleAddon>(_onToggleAddon);
|
||||
on<CreateEventEntityUpdatedEvent>(_onEntityUpdated);
|
||||
on<CreateEventValidateAndPreview>(_onValidateAndPreview);
|
||||
on<DeleteDraftEvent>(_onDeleteDraft);
|
||||
}
|
||||
|
||||
Future<void> _onInit(
|
||||
CreateEventInit event, Emitter<CreateEventState> emit) async {
|
||||
emit(state.copyWith(inLoading: true));
|
||||
late EventEntity entity;
|
||||
bool? isEdit = event.eventModel != null;
|
||||
if (isEdit) {
|
||||
entity = EventEntity.fromEventDto(event.eventModel!);
|
||||
} else {
|
||||
entity = EventEntity.empty();
|
||||
}
|
||||
List<ShiftViewModel> shiftViewModels = [
|
||||
...entity.shifts?.map((shiftEntity) {
|
||||
return ShiftViewModel(
|
||||
id: shiftEntity.id,
|
||||
bloc: CreateShiftDetailsBloc(expanded: !isEdit)
|
||||
..add(CreateShiftInitializeEvent(shiftEntity)),
|
||||
);
|
||||
}).toList() ??
|
||||
[],
|
||||
];
|
||||
|
||||
emit(state.copyWith(
|
||||
recurringType: event.recurringType,
|
||||
entity: entity,
|
||||
tags: [
|
||||
//placeholder for tags - prevent UI jumping
|
||||
TagModel(id: '1', name: ' '),
|
||||
TagModel(id: '2', name: ' '),
|
||||
TagModel(id: '3', name: ' ')
|
||||
],
|
||||
shifts: shiftViewModels));
|
||||
List<Object> results;
|
||||
try {
|
||||
results = await Future.wait([
|
||||
getIt<CreateEventRepository>().getHubs().onError((e, s) {
|
||||
return [];
|
||||
}),
|
||||
getIt<CreateEventRepository>().getAddons().onError((e, s) => []),
|
||||
getIt<CreateEventRepository>().getTags().onError((e, s) => []),
|
||||
getIt<CreateEventRepository>().getContacts().onError((e, s) => []),
|
||||
getIt<CreateEventRepository>().getSkills().onError((e, s) => []),
|
||||
]);
|
||||
} catch (e) {
|
||||
emit(state.copyWith(inLoading: false));
|
||||
return;
|
||||
}
|
||||
var hubs = results[0] as List<HubModel>;
|
||||
var addons = results[1] as List<AddonModel>;
|
||||
var tags = results[2] as List<TagModel>;
|
||||
var contacts = results[3] as List<BusinessMemberModel>;
|
||||
var skills = results[4] as List<BusinessSkillModel>;
|
||||
|
||||
emit(state.copyWith(
|
||||
inLoading: false,
|
||||
hubs: hubs,
|
||||
tags: tags,
|
||||
addons: addons,
|
||||
contacts: contacts,
|
||||
skills: skills));
|
||||
}
|
||||
|
||||
void _onChangeHub(
|
||||
CreateEventChangeHub event, Emitter<CreateEventState> emit) async {
|
||||
if (event.hub == state.entity.hub) return;
|
||||
emit(state.copyWith(
|
||||
entity: state.entity.copyWith(hub: event.hub),
|
||||
));
|
||||
|
||||
state.shifts.forEach((element) {
|
||||
element.bloc
|
||||
.add(CreateShiftAddressSelectEvent(address: event.hub.fullAddress));
|
||||
});
|
||||
|
||||
var departments =
|
||||
await getIt<CreateEventRepository>().getDepartments(event.hub.id);
|
||||
|
||||
emit(state.copyWith(
|
||||
departments: departments,
|
||||
));
|
||||
}
|
||||
|
||||
// void _onChangeContractType(
|
||||
// CreateEventChangeContractType event, Emitter<CreateEventState> emit) {
|
||||
// if (event.contractType == state.entity.contractType) return;
|
||||
// emit(state.copyWith(
|
||||
// entity: state.entity.copyWith(contractType: event.contractType),
|
||||
// ));
|
||||
// }
|
||||
|
||||
// void _onChangeContractNumber(
|
||||
// CreateEventChangeContractNumber event, Emitter<CreateEventState> emit) {
|
||||
// emit(state.copyWith(
|
||||
// entity: state.entity.copyWith(contractNumber: event.contractNumber),
|
||||
// ));
|
||||
// }
|
||||
//
|
||||
void _onChangePoNumber(
|
||||
CreateEventChangePoNumber event, Emitter<CreateEventState> emit) {
|
||||
emit(state.copyWith(
|
||||
entity: state.entity.copyWith(poNumber: event.poNumber),
|
||||
));
|
||||
}
|
||||
|
||||
void _onTagSelected(
|
||||
CreateEventTagSelected event, Emitter<CreateEventState> emit) {
|
||||
final tags = List<TagModel>.of(state.entity.tags ?? []);
|
||||
if (tags.any((e) => e.id == event.tag.id)) {
|
||||
tags.removeWhere((e) => e.id == event.tag.id);
|
||||
} else {
|
||||
tags.add(event.tag);
|
||||
}
|
||||
emit(state.copyWith(
|
||||
entity: state.entity.copyWith(tags: tags),
|
||||
));
|
||||
}
|
||||
|
||||
void _onAddShift(CreateEventAddShift event, Emitter<CreateEventState> emit) {
|
||||
final id = DateTime.now().millisecondsSinceEpoch.toString();
|
||||
ShiftEntity newShiftEntity = ShiftEntity.empty();
|
||||
|
||||
final bloc = CreateShiftDetailsBloc(expanded: true)
|
||||
..add(CreateShiftInitializeEvent(
|
||||
newShiftEntity,
|
||||
));
|
||||
|
||||
newShiftEntity.parentEvent = state.entity;
|
||||
state.entity.shifts?.add(newShiftEntity);
|
||||
|
||||
emit(state.copyWith(
|
||||
shifts: [
|
||||
...state.shifts,
|
||||
ShiftViewModel(id: id, bloc: bloc),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
void _onRemoveShift(
|
||||
CreateEventRemoveShift event, Emitter<CreateEventState> emit) {
|
||||
emit(state.copyWith(
|
||||
entity: state.entity.copyWith(
|
||||
shifts: state.entity.shifts
|
||||
?.where((element) => element.id != event.id)
|
||||
.toList()),
|
||||
shifts: state.shifts.where((element) => element.id != event.id).toList(),
|
||||
));
|
||||
}
|
||||
|
||||
void _onNameChange(
|
||||
CreateEventNameChange event, Emitter<CreateEventState> emit) {
|
||||
emit(state.copyWith(
|
||||
entity: state.entity.copyWith(name: event.value),
|
||||
));
|
||||
}
|
||||
|
||||
void _onAddInfoChange(
|
||||
CreateEventAddInfoChange event, Emitter<CreateEventState> emit) {
|
||||
emit(state.copyWith(
|
||||
entity: state.entity.copyWith(additionalInfo: event.value),
|
||||
));
|
||||
}
|
||||
|
||||
void _onToggleAddon(
|
||||
CreateEventToggleAddon event, Emitter<CreateEventState> emit) {
|
||||
final addons = List<AddonModel>.of(state.entity.addons ?? []);
|
||||
if (addons.any((e) => e.id == event.addon.id)) {
|
||||
addons.removeWhere((e) => e.id == event.addon.id);
|
||||
} else {
|
||||
addons.add(event.addon);
|
||||
}
|
||||
emit(state.copyWith(
|
||||
entity: state.entity.copyWith(addons: addons),
|
||||
));
|
||||
}
|
||||
|
||||
void _onEntityUpdated(
|
||||
CreateEventEntityUpdatedEvent event, Emitter<CreateEventState> emit) {
|
||||
emit(state.copyWith());
|
||||
}
|
||||
|
||||
void _onValidateAndPreview(
|
||||
CreateEventValidateAndPreview event, Emitter<CreateEventState> emit) {
|
||||
var newState = CreateEventInputValidator.validateInputs(
|
||||
state.copyWith(entity: state.entity.copyWith()));
|
||||
emit(newState);
|
||||
emit(newState.copyWith(valid: false));
|
||||
}
|
||||
|
||||
void _onDeleteDraft(
|
||||
DeleteDraftEvent event, Emitter<CreateEventState> emit) async {
|
||||
await getIt<CreateEventService>().deleteDraft(state.entity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
part of 'create_event_bloc.dart';
|
||||
|
||||
@immutable
|
||||
sealed class CreateEventEvent {}
|
||||
|
||||
class CreateEventInit extends CreateEventEvent {
|
||||
final EventModel? eventModel;
|
||||
final EventScheduleType? recurringType;
|
||||
|
||||
CreateEventInit(this.recurringType, this.eventModel);
|
||||
}
|
||||
|
||||
// class CreateEventChangeContractType extends CreateEventEvent {
|
||||
// final EventContractType contractType;
|
||||
//
|
||||
// CreateEventChangeContractType(this.contractType);
|
||||
// }
|
||||
|
||||
class CreateEventChangeHub extends CreateEventEvent {
|
||||
final HubModel hub;
|
||||
|
||||
CreateEventChangeHub(this.hub);
|
||||
}
|
||||
|
||||
class CreateEventValidateAndPreview extends CreateEventEvent {
|
||||
CreateEventValidateAndPreview();
|
||||
}
|
||||
|
||||
// class CreateEventChangeContractNumber extends CreateEventEvent {
|
||||
// final String contractNumber;
|
||||
//
|
||||
// CreateEventChangeContractNumber(this.contractNumber);
|
||||
// }
|
||||
|
||||
class CreateEventChangePoNumber extends CreateEventEvent {
|
||||
final String poNumber;
|
||||
|
||||
CreateEventChangePoNumber(this.poNumber);
|
||||
}
|
||||
|
||||
class CreateEventNameChange extends CreateEventEvent {
|
||||
final String value;
|
||||
|
||||
CreateEventNameChange(this.value);
|
||||
}
|
||||
|
||||
class CreateEventAddInfoChange extends CreateEventEvent {
|
||||
final String value;
|
||||
|
||||
CreateEventAddInfoChange(this.value);
|
||||
}
|
||||
|
||||
class CreateEventTagSelected extends CreateEventEvent {
|
||||
final TagModel tag;
|
||||
|
||||
CreateEventTagSelected(this.tag);
|
||||
}
|
||||
|
||||
class CreateEventAddShift extends CreateEventEvent {
|
||||
CreateEventAddShift();
|
||||
}
|
||||
|
||||
class CreateEventRemoveShift extends CreateEventEvent {
|
||||
final String id;
|
||||
|
||||
CreateEventRemoveShift(this.id);
|
||||
}
|
||||
|
||||
class CreateEventToggleAddon extends CreateEventEvent {
|
||||
final AddonModel addon;
|
||||
|
||||
CreateEventToggleAddon(this.addon);
|
||||
}
|
||||
|
||||
class CreateEventEntityUpdatedEvent extends CreateEventEvent {
|
||||
CreateEventEntityUpdatedEvent();
|
||||
}
|
||||
|
||||
class DeleteDraftEvent extends CreateEventEvent {
|
||||
DeleteDraftEvent();
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
part of 'create_event_bloc.dart';
|
||||
|
||||
@immutable
|
||||
class CreateEventState {
|
||||
final bool inLoading;
|
||||
final bool success;
|
||||
final bool valid;
|
||||
final EventEntity entity;
|
||||
final EventScheduleType recurringType;
|
||||
final List<HubModel> hubs;
|
||||
final List<TagModel> tags;
|
||||
final List<BusinessMemberModel> contacts;
|
||||
final List<ShiftViewModel> shifts;
|
||||
final List<AddonModel> addons;
|
||||
final List<BusinessSkillModel> skills;
|
||||
final List<DepartmentModel> departments;
|
||||
final EventValidationState? validationState;
|
||||
|
||||
final bool showEmptyFieldError;
|
||||
|
||||
const CreateEventState(
|
||||
{required this.entity,
|
||||
this.inLoading = false,
|
||||
this.valid = false,
|
||||
this.showEmptyFieldError = false,
|
||||
this.success = false,
|
||||
this.recurringType = EventScheduleType.oneTime,
|
||||
this.hubs = const [],
|
||||
this.tags = const [],
|
||||
this.contacts = const [],
|
||||
this.addons = const [],
|
||||
this.skills = const [],
|
||||
this.shifts = const [],
|
||||
this.departments = const [],
|
||||
this.validationState});
|
||||
|
||||
CreateEventState copyWith({
|
||||
bool? inLoading,
|
||||
bool? success,
|
||||
bool? valid,
|
||||
bool? showEmptyFieldError,
|
||||
EventEntity? entity,
|
||||
EventScheduleType? recurringType,
|
||||
List<HubModel>? hubs,
|
||||
List<TagModel>? tags,
|
||||
List<BusinessMemberModel>? contacts,
|
||||
List<ShiftViewModel>? shifts,
|
||||
List<AddonModel>? addons,
|
||||
List<BusinessSkillModel>? skills,
|
||||
List<DepartmentModel>? departments,
|
||||
EventValidationState? validationState,
|
||||
}) {
|
||||
return CreateEventState(
|
||||
success: success ?? this.success,
|
||||
valid: valid ?? this.valid,
|
||||
inLoading: inLoading ?? this.inLoading,
|
||||
showEmptyFieldError: showEmptyFieldError ?? this.showEmptyFieldError,
|
||||
entity: entity ?? this.entity,
|
||||
recurringType: recurringType ?? this.recurringType,
|
||||
hubs: hubs ?? this.hubs,
|
||||
tags: tags ?? this.tags,
|
||||
contacts: contacts ?? this.contacts,
|
||||
shifts: shifts ?? this.shifts,
|
||||
addons: addons ?? this.addons,
|
||||
skills: skills ?? this.skills,
|
||||
departments: departments ?? this.departments,
|
||||
validationState: validationState,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ShiftViewModel {
|
||||
final String id;
|
||||
final CreateShiftDetailsBloc bloc;
|
||||
|
||||
ShiftViewModel({required this.id, required this.bloc});
|
||||
}
|
||||
|
||||
class EventValidationState {
|
||||
final String? nameError;
|
||||
final String? startDateError;
|
||||
final String? endDateError;
|
||||
final String? hubError;
|
||||
// final String? contractNumberError;
|
||||
final String? poNumberError;
|
||||
final String? shiftsError;
|
||||
|
||||
bool showed = false;
|
||||
|
||||
bool get hasError =>
|
||||
nameError != null ||
|
||||
startDateError != null ||
|
||||
endDateError != null ||
|
||||
hubError != null ||
|
||||
// contractNumberError != null ||
|
||||
poNumberError != null ||
|
||||
shiftsError != null;
|
||||
|
||||
String? get message {
|
||||
return nameError ??
|
||||
startDateError ??
|
||||
endDateError ??
|
||||
hubError ??
|
||||
// contractNumberError ??
|
||||
poNumberError ??
|
||||
shiftsError ??
|
||||
'';
|
||||
}
|
||||
|
||||
EventValidationState(
|
||||
{this.nameError,
|
||||
this.startDateError,
|
||||
this.endDateError,
|
||||
this.hubError,
|
||||
// this.contractNumberError,
|
||||
this.poNumberError,
|
||||
this.shiftsError});
|
||||
|
||||
EventValidationState copyWith({
|
||||
String? nameError,
|
||||
String? startDateError,
|
||||
String? endDateError,
|
||||
String? hubError,
|
||||
// String? contractNumberError,
|
||||
String? poNumberError,
|
||||
String? shiftsError,
|
||||
}) {
|
||||
return EventValidationState(
|
||||
nameError: nameError ?? this.nameError,
|
||||
startDateError: startDateError ?? this.startDateError,
|
||||
endDateError: endDateError ?? this.endDateError,
|
||||
hubError: hubError ?? this.hubError,
|
||||
// contractNumberError: contractNumberError ?? this.contractNumberError,
|
||||
poNumberError: poNumberError ?? this.poNumberError,
|
||||
shiftsError: shiftsError ?? this.shiftsError,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import 'package:krow/core/data/models/event/addon_model.dart';
|
||||
import 'package:krow/core/data/models/event/business_member_model.dart';
|
||||
import 'package:krow/core/data/models/event/hub_model.dart';
|
||||
import 'package:krow/core/data/models/event/tag_model.dart';
|
||||
import 'package:krow/core/data/models/shift/business_skill_model.dart';
|
||||
import 'package:krow/core/data/models/shift/department_model.dart';
|
||||
|
||||
abstract class CreateEventRepository {
|
||||
Future<List<HubModel>> getHubs();
|
||||
|
||||
Future<List<AddonModel>> getAddons();
|
||||
|
||||
Future<List<TagModel>> getTags();
|
||||
|
||||
Future<List<BusinessMemberModel>> getContacts();
|
||||
|
||||
Future<List<BusinessSkillModel>> getSkills();
|
||||
|
||||
Future<List<DepartmentModel>> getDepartments(String hubId);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
@singleton
|
||||
class GooglePlacesService {
|
||||
Future<List<MapPlace>> fetchSuggestions(String query) async {
|
||||
final String apiKey = dotenv.env['GOOGLE_MAP']!;
|
||||
|
||||
const String baseUrl =
|
||||
'https://maps.googleapis.com/maps/api/place/autocomplete/json';
|
||||
final Uri uri =
|
||||
Uri.parse('$baseUrl?input=$query&key=$apiKey&types=geocode');
|
||||
|
||||
final response = await http.get(uri);
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
final List<dynamic> predictions = data['predictions'];
|
||||
|
||||
return predictions.map((prediction) {
|
||||
return MapPlace.fromJson(prediction);
|
||||
}).toList();
|
||||
} else {
|
||||
throw Exception('Failed to fetch place suggestions');
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getPlaceDetails(String placeId) async {
|
||||
final String apiKey = dotenv.env['GOOGLE_MAP']!;
|
||||
final String url =
|
||||
'https://maps.googleapis.com/maps/api/place/details/json?place_id=$placeId&key=$apiKey';
|
||||
|
||||
final response = await http.get(Uri.parse(url));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
final result = data['result'];
|
||||
final location = result['geometry']['location'];
|
||||
|
||||
Map<String, dynamic> addressDetails = {
|
||||
'lat': location['lat'], // Latitude
|
||||
'lng': location['lng'], // Longitude
|
||||
'formatted_address': result['formatted_address'], // Full Address
|
||||
};
|
||||
|
||||
for (var component in result['address_components']) {
|
||||
List types = component['types'];
|
||||
if (types.contains('street_number')) {
|
||||
addressDetails['street_number'] = component['long_name'];
|
||||
}
|
||||
if (types.contains('route')) {
|
||||
addressDetails['street'] = component['long_name'];
|
||||
}
|
||||
if (types.contains('locality')) {
|
||||
addressDetails['city'] = component['long_name'];
|
||||
}
|
||||
if (types.contains('administrative_area_level_1')) {
|
||||
addressDetails['state'] = component['long_name'];
|
||||
}
|
||||
if (types.contains('country')) {
|
||||
addressDetails['country'] = component['long_name'];
|
||||
}
|
||||
if (types.contains('postal_code')) {
|
||||
addressDetails['postal_code'] = component['long_name'];
|
||||
}
|
||||
}
|
||||
|
||||
return addressDetails;
|
||||
} else {
|
||||
throw Exception('Failed to fetch place details');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MapPlace {
|
||||
final String description;
|
||||
final String placeId;
|
||||
|
||||
MapPlace({required this.description, required this.placeId});
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
'description': description,
|
||||
'place_id': placeId,
|
||||
};
|
||||
}
|
||||
|
||||
factory MapPlace.fromJson(Map<String, dynamic> json) {
|
||||
return MapPlace(
|
||||
description: json['description'],
|
||||
placeId: json['place_id'],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import 'package:krow/core/data/models/event/event_model.dart';
|
||||
import 'package:krow/features/create_event/domain/bloc/create_event_bloc.dart';
|
||||
import 'package:krow/features/create_event/presentation/create_role_section/bloc/create_role_bloc.dart';
|
||||
import 'package:krow/features/create_event/presentation/create_shift_details_section/bloc/create_shift_details_bloc.dart';
|
||||
|
||||
class CreateEventInputValidator {
|
||||
static CreateEventState validateInputs(CreateEventState state) {
|
||||
var newState = _validateEvent(state);
|
||||
newState = _validateShifts(newState);
|
||||
return newState;
|
||||
}
|
||||
|
||||
static CreateEventState _validateEvent(CreateEventState state) {
|
||||
EventValidationState? validationState;
|
||||
if (state.entity.name.isEmpty) {
|
||||
validationState = EventValidationState(nameError: 'Name cannot be empty');
|
||||
}
|
||||
|
||||
if (state.entity.startDate == null) {
|
||||
validationState =
|
||||
EventValidationState(startDateError: 'Start Date cannot be empty');
|
||||
}
|
||||
|
||||
if (state.recurringType == EventScheduleType.recurring &&
|
||||
state.entity.endDate == null) {
|
||||
validationState =
|
||||
EventValidationState(endDateError: 'End Date cannot be empty');
|
||||
}
|
||||
if (state.entity.hub == null) {
|
||||
validationState = EventValidationState(hubError: 'Hub cannot be empty');
|
||||
}
|
||||
// if (state.entity.contractType == EventContractType.contract &&
|
||||
// (state.entity.contractNumber?.isEmpty ?? true)) {
|
||||
// validationState = EventValidationState(
|
||||
// contractNumberError: 'Contract Number cannot be empty');
|
||||
// }
|
||||
if ((state.entity.poNumber?.isEmpty ?? true)) {
|
||||
validationState =
|
||||
EventValidationState(poNumberError: 'PO Number cannot be empty');
|
||||
}
|
||||
|
||||
if (state.shifts.isEmpty) {
|
||||
validationState =
|
||||
EventValidationState(shiftsError: 'Shifts cannot be empty');
|
||||
}
|
||||
|
||||
if (state.validationState == null && validationState == null) {
|
||||
return state.copyWith(valid: true);
|
||||
} else {
|
||||
return state.copyWith(validationState: validationState);
|
||||
}
|
||||
}
|
||||
|
||||
static CreateEventState _validateShifts(CreateEventState state) {
|
||||
for (var shift in state.shifts) {
|
||||
ShiftValidationState? validationState;
|
||||
if (!(shift.bloc.state.shift.fullAddress?.isValid() ?? false)) {
|
||||
validationState = ShiftValidationState(addressError: 'Invalid Address');
|
||||
}
|
||||
|
||||
if (shift.bloc.state.shift.managers.isEmpty) {
|
||||
validationState =
|
||||
ShiftValidationState(contactsError: 'Managers cannot be empty');
|
||||
}
|
||||
|
||||
if (validationState != null) {
|
||||
shift.bloc.add(ValidationFailedEvent(validationState));
|
||||
return state.copyWith(
|
||||
valid: false, validationState: state.validationState);
|
||||
}
|
||||
|
||||
if (validatePosition(shift.bloc.state.roles)) {
|
||||
return state.copyWith(
|
||||
valid: false, validationState: state.validationState);
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
static bool validatePosition(List<RoleViewModel> roles) {
|
||||
for (var position in roles) {
|
||||
PositionValidationState? validationState;
|
||||
|
||||
if (position.bloc.state.entity.businessSkill == null) {
|
||||
validationState =
|
||||
PositionValidationState(skillError: 'Skill cannot be empty');
|
||||
}
|
||||
|
||||
if (position.bloc.state.entity.department == null) {
|
||||
validationState = PositionValidationState(
|
||||
departmentError: 'Department cannot be empty');
|
||||
}
|
||||
|
||||
if (position.bloc.state.entity.count == null) {
|
||||
validationState =
|
||||
PositionValidationState(countError: 'Count cannot be empty');
|
||||
}
|
||||
|
||||
if (validationState != null) {
|
||||
position.bloc.add(ValidationPositionFailedEvent(validationState));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/application/routing/routes.gr.dart';
|
||||
import 'package:krow/core/data/models/event/event_model.dart';
|
||||
import 'package:krow/core/entity/event_entity.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_app_bar.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_popup_button.dart';
|
||||
import 'package:krow/features/create_event/domain/bloc/create_event_bloc.dart';
|
||||
import 'package:krow/features/create_event/presentation/create_shift_details_section/create_shifts_list.dart';
|
||||
import 'package:krow/features/create_event/presentation/event_date_section/bloc/date_selector_bloc.dart';
|
||||
import 'package:krow/features/create_event/presentation/widgets/add_info_input_widget.dart';
|
||||
import 'package:krow/features/create_event/presentation/widgets/addons_section_widget.dart';
|
||||
import 'package:krow/features/create_event/presentation/widgets/create_event_details_card_widget.dart';
|
||||
import 'package:krow/features/create_event/presentation/widgets/create_event_tags_card.dart';
|
||||
import 'package:krow/features/create_event/presentation/widgets/create_event_title_widget.dart';
|
||||
import 'package:krow/features/create_event/presentation/widgets/total_cost_row_widget.dart';
|
||||
import 'package:modal_progress_hud_nsn/modal_progress_hud_nsn.dart';
|
||||
|
||||
@RoutePage()
|
||||
class CreateEventScreen extends StatelessWidget implements AutoRouteWrapper {
|
||||
final EventScheduleType? eventType;
|
||||
final EventModel? eventModel;
|
||||
|
||||
const CreateEventScreen({super.key, this.eventType, this.eventModel});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<CreateEventBloc, CreateEventState>(
|
||||
listener: (context, state) {
|
||||
if (state.success) {
|
||||
context.router.maybePop();
|
||||
}
|
||||
|
||||
if (state.valid) {
|
||||
context.router
|
||||
.push(EventDetailsRoute(event: state.entity, isPreview: true));
|
||||
}
|
||||
_checkValidation(state, context);
|
||||
},
|
||||
builder: (context, state) {
|
||||
return ModalProgressHUD(
|
||||
inAsyncCall: state.inLoading,
|
||||
child: Scaffold(
|
||||
appBar: KwAppBar(
|
||||
titleText: 'Create Event',
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: CustomScrollView(
|
||||
primary: false,
|
||||
slivers: [
|
||||
SliverList.list(
|
||||
children: const [
|
||||
CreateEventTitleWidget(),
|
||||
CreateEventDetailsCardWidget(),
|
||||
CreateEventTagsCard(),
|
||||
],
|
||||
),
|
||||
const CreateShiftsList(),
|
||||
const SliverGap(24),
|
||||
sliverWrap(
|
||||
KwButton.outlinedPrimary(
|
||||
onPressed: () {
|
||||
BlocProvider.of<CreateEventBloc>(context)
|
||||
.add(CreateEventAddShift());
|
||||
},
|
||||
label: 'Add Shift',
|
||||
leftIcon: Assets.images.icons.add,
|
||||
),
|
||||
),
|
||||
const SliverGap(24),
|
||||
sliverWrap(const AddonsSectionWidget()),
|
||||
const SliverGap(24),
|
||||
sliverWrap(const AddInfoInputWidget()),
|
||||
const SliverGap(24),
|
||||
sliverWrap( TotalCostRowWidget()),
|
||||
const SliverGap(24),
|
||||
SliverSafeArea(
|
||||
top: false,
|
||||
sliver: SliverPadding(
|
||||
padding: const EdgeInsets.only(bottom: 36, top: 36),
|
||||
sliver: sliverWrap(
|
||||
KwPopUpButton(
|
||||
disabled: state.validationState != null,
|
||||
label: state.entity.id.isEmpty
|
||||
? 'Save as Draft'
|
||||
: state.entity.status == EventStatus.draft
|
||||
? 'Update Draft'
|
||||
: 'Update Event',
|
||||
popUpPadding: 16,
|
||||
items: [
|
||||
KwPopUpButtonItem(
|
||||
title: 'Preview',
|
||||
onTap: () {
|
||||
BlocProvider.of<CreateEventBloc>(context)
|
||||
.add(CreateEventValidateAndPreview());
|
||||
}),
|
||||
if (state.entity.status == EventStatus.draft)
|
||||
KwPopUpButtonItem(
|
||||
title: 'Delete Event Draft',
|
||||
onTap: () {
|
||||
BlocProvider.of<CreateEventBloc>(context)
|
||||
.add(DeleteDraftEvent());
|
||||
context.router.popUntilRoot();
|
||||
context.router.maybePop();
|
||||
},
|
||||
color: AppColors.statusError),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
sliverWrap(Widget widget) {
|
||||
return SliverToBoxAdapter(
|
||||
child: widget,
|
||||
);
|
||||
}
|
||||
|
||||
void _checkValidation(CreateEventState state, BuildContext context) {
|
||||
if (state.validationState != null && !state.validationState!.showed) {
|
||||
state.validationState?.showed = true;
|
||||
for (var e in state.shifts) {
|
||||
e.bloc.state.validationState?.showed = true;
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(state.validationState?.message ?? ''),
|
||||
backgroundColor: AppColors.statusError,
|
||||
));
|
||||
} else {
|
||||
var invalidShift = state.shifts
|
||||
.firstWhereOrNull((e) => e.bloc.state.validationState != null);
|
||||
|
||||
if (invalidShift != null &&
|
||||
!invalidShift.bloc.state.validationState!.showed) {
|
||||
invalidShift.bloc.state.validationState?.showed = true;
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(invalidShift.bloc.state.validationState?.message ?? ''),
|
||||
backgroundColor: AppColors.statusError,
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
var invalidRole = state.shifts
|
||||
.expand((shift) => shift.bloc.state.roles)
|
||||
.firstWhereOrNull((role) => role.bloc.state.validationState != null);
|
||||
|
||||
if(invalidRole != null && !invalidRole.bloc.state.validationState!.showed) {
|
||||
invalidRole.bloc.state.validationState?.showed = true;
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(invalidRole.bloc.state.validationState?.message ?? ''),
|
||||
backgroundColor: AppColors.statusError,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget wrappedRoute(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (_) =>
|
||||
CreateEventBloc()..add(CreateEventInit(eventType, eventModel))),
|
||||
BlocProvider(
|
||||
create: (_) => DateSelectorBloc()
|
||||
..add(DateSelectorEventInit(eventType, eventModel))),
|
||||
],
|
||||
child: this,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/core/data/models/event/skill.dart';
|
||||
import 'package:krow/core/data/models/shift/business_skill_model.dart';
|
||||
import 'package:krow/core/data/models/shift/department_model.dart';
|
||||
import 'package:krow/core/entity/position_entity.dart';
|
||||
import 'package:krow/core/entity/role_schedule_entity.dart';
|
||||
|
||||
part 'create_role_event.dart';
|
||||
part 'create_role_state.dart';
|
||||
|
||||
class CreateRoleBloc extends Bloc<CreateRoleEvent, CreateRoleState> {
|
||||
CreateRoleBloc()
|
||||
: super(CreateRoleState(
|
||||
isExpanded: true,
|
||||
entity: PositionEntity.empty(),
|
||||
)) {
|
||||
on<CreateRoleInitEvent>(_onRoleInit);
|
||||
on<CreateRoleSelectSkillEvent>(_onRoleSelect);
|
||||
on<ExpandRoleEvent>(_onExpandRole);
|
||||
on<SetRoleStartTimeEvent>(_onSetRoleStartTime);
|
||||
on<SetRoleEndTimeEvent>(_onSetRoleEndTime);
|
||||
on<CreateRoleSelectDepartmentEvent>(_onSelectDepartment);
|
||||
on<CreateRoleSelectCountEvent>(_onSelectCount);
|
||||
on<CreateRoleSetScheduleEvent>(_onSetRoleSchedule);
|
||||
on<CreateRoleSelectBreak>(_onSetRoleBreak);
|
||||
on<ValidationPositionFailedEvent>(_onValidationFailed);
|
||||
}
|
||||
|
||||
FutureOr<void> _onRoleInit(CreateRoleInitEvent event, emit) {
|
||||
emit(state.copyWith(entity: event.role));
|
||||
}
|
||||
|
||||
FutureOr<void> _onRoleSelect(CreateRoleSelectSkillEvent event, emit) {
|
||||
emit(state.copyWith(entity: state.entity.copyWith(businessSkill: event.skill)));
|
||||
}
|
||||
|
||||
FutureOr<void> _onExpandRole(event, emit) {
|
||||
emit(state.copyWith(isExpanded: !state.isExpanded));
|
||||
}
|
||||
|
||||
FutureOr<void> _onSetRoleStartTime(event, emit) {
|
||||
emit(state.copyWith(
|
||||
entity: state.entity.copyWith(startTime: event.startTime)));
|
||||
}
|
||||
|
||||
FutureOr<void> _onSetRoleEndTime(event, emit) {
|
||||
emit(state.copyWith(entity: state.entity.copyWith(endTime: event.endTime)));
|
||||
}
|
||||
|
||||
FutureOr<void> _onSelectDepartment(
|
||||
CreateRoleSelectDepartmentEvent event, emit) {
|
||||
emit(state.copyWith(entity: state.entity.copyWith(department: event.item)));
|
||||
}
|
||||
|
||||
FutureOr<void> _onSelectCount(CreateRoleSelectCountEvent event, emit) {
|
||||
emit(state.copyWith(entity: state.entity.copyWith(count: event.item)));
|
||||
}
|
||||
|
||||
FutureOr<void> _onSetRoleSchedule(CreateRoleSetScheduleEvent event, emit) {
|
||||
emit(state.copyWith(
|
||||
entity: state.entity.copyWith(schedule: event.schedule)));
|
||||
}
|
||||
|
||||
FutureOr<void> _onSetRoleBreak(CreateRoleSelectBreak event, emit) {
|
||||
emit(state.copyWith(entity: state.entity.copyWith(breakDuration: event.item)));
|
||||
}
|
||||
|
||||
FutureOr<void> _onValidationFailed(ValidationPositionFailedEvent event, emit) {
|
||||
emit(state.copyWith(validationState: event.validationState));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
part of 'create_role_bloc.dart';
|
||||
|
||||
@immutable
|
||||
sealed class CreateRoleEvent {}
|
||||
|
||||
class CreateRoleInitEvent extends CreateRoleEvent {
|
||||
final PositionEntity role;
|
||||
|
||||
CreateRoleInitEvent(this.role);
|
||||
}
|
||||
|
||||
class CreateRoleSelectSkillEvent extends CreateRoleEvent {
|
||||
final BusinessSkillModel skill;
|
||||
|
||||
CreateRoleSelectSkillEvent(this.skill);
|
||||
}
|
||||
|
||||
class ExpandRoleEvent extends CreateRoleEvent {
|
||||
ExpandRoleEvent();
|
||||
}
|
||||
|
||||
class SetRoleStartTimeEvent extends CreateRoleEvent {
|
||||
final DateTime startTime;
|
||||
|
||||
SetRoleStartTimeEvent(this.startTime);
|
||||
}
|
||||
|
||||
class SetRoleEndTimeEvent extends CreateRoleEvent {
|
||||
final DateTime endTime;
|
||||
|
||||
SetRoleEndTimeEvent(this.endTime);
|
||||
}
|
||||
|
||||
class CreateRoleSelectDepartmentEvent extends CreateRoleEvent {
|
||||
final DepartmentModel item;
|
||||
|
||||
CreateRoleSelectDepartmentEvent(this.item);
|
||||
}
|
||||
|
||||
class CreateRoleSelectCountEvent extends CreateRoleEvent {
|
||||
final int item;
|
||||
|
||||
CreateRoleSelectCountEvent(this.item);
|
||||
}
|
||||
|
||||
class CreateRoleSelectBreak extends CreateRoleEvent {
|
||||
final int item;
|
||||
|
||||
CreateRoleSelectBreak(this.item);
|
||||
}
|
||||
|
||||
class CreateRoleSetScheduleEvent extends CreateRoleEvent {
|
||||
final List<RoleScheduleEntity> schedule;
|
||||
|
||||
CreateRoleSetScheduleEvent(this.schedule);
|
||||
}
|
||||
|
||||
class ValidationPositionFailedEvent extends CreateRoleEvent {
|
||||
final PositionValidationState validationState;
|
||||
|
||||
ValidationPositionFailedEvent(this.validationState);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
part of 'create_role_bloc.dart';
|
||||
|
||||
@immutable
|
||||
class CreateRoleState {
|
||||
final bool isExpanded;
|
||||
final PositionEntity entity;
|
||||
final PositionValidationState? validationState;
|
||||
|
||||
const CreateRoleState({
|
||||
this.isExpanded = true,
|
||||
required this.entity,
|
||||
this.validationState,
|
||||
});
|
||||
|
||||
CreateRoleState copyWith(
|
||||
{bool? isExpanded,
|
||||
PositionEntity? entity,
|
||||
PositionValidationState? validationState}) {
|
||||
return CreateRoleState(
|
||||
isExpanded: isExpanded ?? this.isExpanded,
|
||||
entity: entity ?? this.entity,
|
||||
validationState: validationState,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PositionValidationState {
|
||||
final String? skillError;
|
||||
final String? departmentError;
|
||||
final String? countError;
|
||||
bool showed;
|
||||
|
||||
bool get hasError =>
|
||||
skillError != null || countError != null || departmentError != null;
|
||||
|
||||
String? get message {
|
||||
return skillError ?? countError ?? departmentError ?? '';
|
||||
}
|
||||
|
||||
PositionValidationState(
|
||||
{this.skillError,
|
||||
this.countError,
|
||||
this.departmentError,
|
||||
this.showed = false});
|
||||
|
||||
PositionValidationState copyWith({
|
||||
String? skillError,
|
||||
String? countError,
|
||||
String? departmentError,
|
||||
}) {
|
||||
return PositionValidationState(
|
||||
skillError: skillError ?? this.skillError,
|
||||
countError: countError ?? this.countError,
|
||||
departmentError: departmentError ?? this.departmentError,
|
||||
showed: showed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
import 'package:expandable/expandable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/data/models/event/event_model.dart';
|
||||
import 'package:krow/core/data/models/event/skill.dart';
|
||||
import 'package:krow/core/data/models/shift/business_skill_model.dart';
|
||||
import 'package:krow/core/data/models/shift/department_model.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/kw_time_slot.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_dropdown.dart';
|
||||
import 'package:krow/features/create_event/domain/bloc/create_event_bloc.dart';
|
||||
import 'package:krow/features/create_event/presentation/create_role_section/bloc/create_role_bloc.dart';
|
||||
import 'package:krow/features/create_event/presentation/create_shift_details_section/bloc/create_shift_details_bloc.dart';
|
||||
import 'package:krow/features/create_event/presentation/role_schedule_dialog/recurring_schedule_widget.dart';
|
||||
|
||||
class CreateRoleDetailsWidget extends StatefulWidget {
|
||||
final String id;
|
||||
|
||||
const CreateRoleDetailsWidget({
|
||||
super.key,
|
||||
required this.id,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CreateRoleDetailsWidget> createState() =>
|
||||
_CreateRoleDetailsWidgetState();
|
||||
}
|
||||
|
||||
class _CreateRoleDetailsWidgetState extends State<CreateRoleDetailsWidget> {
|
||||
ExpandableController? _expandableController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_expandableController = ExpandableController(initialExpanded: true);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_expandableController?.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CreateEventBloc, CreateEventState>(
|
||||
builder: (context, eventState) {
|
||||
return BlocBuilder<CreateShiftDetailsBloc, CreateShiftDetailsState>(
|
||||
builder: (context, shiftState) {
|
||||
return BlocConsumer<CreateRoleBloc, CreateRoleState>(
|
||||
listener: (context, state) {
|
||||
if (state.isExpanded != _expandableController?.expanded) {
|
||||
_expandableController!.toggle();
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.only(top: 12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: KwBoxDecorations.primaryLight8.copyWith(
|
||||
border: state.validationState != null
|
||||
? Border.all(color: AppColors.statusError, width: 1)
|
||||
: null),
|
||||
child: ExpandableTheme(
|
||||
data: const ExpandableThemeData(
|
||||
hasIcon: false,
|
||||
animationDuration: Duration(milliseconds: 250)),
|
||||
child: ExpandablePanel(
|
||||
collapsed: Container(),
|
||||
controller: _expandableController,
|
||||
header: _buildHeader(state, shiftState, context),
|
||||
expanded: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_skillDropDown(state.entity.businessSkill,
|
||||
eventState.skills, context),
|
||||
const Gap(8),
|
||||
_departmentDropDown(state.entity.department,
|
||||
eventState.departments, context),
|
||||
const Gap(8),
|
||||
_timeRow(context, state.entity.startTime,
|
||||
state.entity.endTime),
|
||||
const Gap(8),
|
||||
_countDropDown(state.entity.count, context),
|
||||
const Gap(8),
|
||||
_breakDropDown(context, state),
|
||||
if (eventState.recurringType ==
|
||||
EventScheduleType.recurring)
|
||||
const RecurringScheduleWidget(),
|
||||
const Gap(12),
|
||||
textRow('Cost',
|
||||
'\$${state.entity.businessSkill?.price?.toStringAsFixed(2) ?? '0'}'),
|
||||
const Gap(12),
|
||||
textRow('Value', '\$${state.entity.price.toStringAsFixed(2)}'),
|
||||
const Gap(12),
|
||||
KwButton.accent(
|
||||
label: 'Save Role',
|
||||
onPressed: () {
|
||||
context
|
||||
.read<CreateRoleBloc>()
|
||||
.add(ExpandRoleEvent());
|
||||
}),
|
||||
const Gap(12),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(CreateRoleState state, CreateShiftDetailsState shiftState,
|
||||
BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
context.read<CreateRoleBloc>().add(ExpandRoleEvent());
|
||||
},
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
padding: const EdgeInsets.only(bottom: 12, top: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
state.entity.businessSkill?.skill?.name ?? 'New Role',
|
||||
style: AppTextStyles.bodyLargeMed,
|
||||
),
|
||||
if (!state.isExpanded)
|
||||
Center(
|
||||
child: Assets.images.icons.chevronDown.svg(
|
||||
height: 24,
|
||||
width: 24,
|
||||
colorFilter: const ColorFilter.mode(
|
||||
AppColors.grayStroke,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (state.isExpanded && shiftState.roles.length > 1)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context
|
||||
.read<CreateShiftDetailsBloc>()
|
||||
.add(DeleteRoleDeleteEvent(widget.id));
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 4),
|
||||
child: Assets.images.icons.delete.svg(
|
||||
width: 16,
|
||||
height: 16,
|
||||
colorFilter: const ColorFilter.mode(
|
||||
AppColors.statusError, BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_skillDropDown(BusinessSkillModel? selectedSkill,
|
||||
List<BusinessSkillModel> allSkills, BuildContext context) {
|
||||
return KwDropdown(
|
||||
key: ValueKey('skill_key${widget.id}'),
|
||||
title: 'Role',
|
||||
hintText: 'Role name',
|
||||
horizontalPadding: 40,
|
||||
selectedItem: selectedSkill != null
|
||||
? KwDropDownItem(
|
||||
data: selectedSkill, title: selectedSkill.skill?.name ?? '')
|
||||
: null,
|
||||
items: allSkills
|
||||
.map((e) => KwDropDownItem(data: e, title: e.skill?.name ?? '')),
|
||||
onSelected: (item) {
|
||||
context.read<CreateRoleBloc>().add(CreateRoleSelectSkillEvent(item));
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 500),
|
||||
() => context
|
||||
.read<CreateEventBloc>()
|
||||
.add(CreateEventEntityUpdatedEvent()));
|
||||
});
|
||||
}
|
||||
|
||||
_departmentDropDown(DepartmentModel? selected,
|
||||
List<DepartmentModel> allDepartments, BuildContext context) {
|
||||
return KwDropdown(
|
||||
title: 'Department',
|
||||
hintText: 'Department name',
|
||||
horizontalPadding: 40,
|
||||
selectedItem: selected != null
|
||||
? KwDropDownItem(data: selected, title: selected.name)
|
||||
: null,
|
||||
items:
|
||||
allDepartments.map((e) => KwDropDownItem(data: e, title: e.name)),
|
||||
onSelected: (item) {
|
||||
context
|
||||
.read<CreateRoleBloc>()
|
||||
.add(CreateRoleSelectDepartmentEvent(item));
|
||||
});
|
||||
}
|
||||
|
||||
_countDropDown(int? selected, BuildContext context) {
|
||||
return KwDropdown(
|
||||
key: ValueKey('count_key${widget.id}'),
|
||||
title: 'Number of Employee for one Role',
|
||||
hintText: 'Person count',
|
||||
horizontalPadding: 40,
|
||||
selectedItem: selected != null
|
||||
? KwDropDownItem(data: selected, title: selected.toString())
|
||||
: null,
|
||||
items: List.generate(98, (e) => e + 1)
|
||||
.map((e) => KwDropDownItem(data: e, title: (e).toString())),
|
||||
onSelected: (item) {
|
||||
context.read<CreateRoleBloc>().add(CreateRoleSelectCountEvent(item));
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 500),
|
||||
() {
|
||||
context
|
||||
.read<CreateEventBloc>()
|
||||
.add(CreateEventEntityUpdatedEvent());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_timeRow(context, DateTime? startTime, DateTime? endTime) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: KwTimeSlotInput(
|
||||
key: ValueKey('start_time_key${widget.id}'),
|
||||
label: 'Start Time',
|
||||
initialValue: startTime ?? DateTime(2000,1,1,9,0),
|
||||
onChange: (value) {
|
||||
|
||||
BlocProvider.of<CreateRoleBloc>(context)
|
||||
.add(SetRoleStartTimeEvent(value));
|
||||
try {
|
||||
// Future.delayed(
|
||||
// const Duration(milliseconds: 100),
|
||||
// () => context
|
||||
// .read<CreateEventBloc>()
|
||||
// .add(CreateEventEntityUpdatedEvent()));
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: KwTimeSlotInput(
|
||||
key: ValueKey('end_time_key${widget.id}'),
|
||||
label: 'End Time',
|
||||
initialValue: endTime ?? DateTime(2000,1,1,14,0),
|
||||
onChange: (value) {
|
||||
BlocProvider.of<CreateRoleBloc>(context)
|
||||
.add(SetRoleEndTimeEvent(value));
|
||||
try {
|
||||
// Future.delayed(
|
||||
// const Duration(milliseconds: 100),
|
||||
// () => context
|
||||
// .read<CreateEventBloc>()
|
||||
// .add(CreateEventEntityUpdatedEvent()));
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
_breakDropDown(BuildContext context,CreateRoleState state) {
|
||||
String formatBreakDuration(int minutes) {
|
||||
if (minutes == 60) {
|
||||
return '1h';
|
||||
} else if (minutes > 60) {
|
||||
final hours = minutes ~/ 60;
|
||||
final remainder = minutes % 60;
|
||||
return remainder == 0 ? '${hours}h' : '${hours}h ${remainder} min';
|
||||
}
|
||||
return '${minutes} min';
|
||||
}
|
||||
|
||||
|
||||
|
||||
return KwDropdown(
|
||||
key: ValueKey('break_key${widget.id}'),
|
||||
title: 'Break Duration',
|
||||
hintText: 'Duration',
|
||||
horizontalPadding: 40,
|
||||
selectedItem: KwDropDownItem(data: state.entity.breakDuration, title: formatBreakDuration(state.entity.breakDuration??0)),
|
||||
items: [15, 30].map((e) {
|
||||
return KwDropDownItem(data: e, title: formatBreakDuration(e));
|
||||
}),
|
||||
onSelected: (item) {
|
||||
context.read<CreateRoleBloc>().add(CreateRoleSelectBreak(item??0));
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
Widget textRow(String key, String value) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(key,
|
||||
style: AppTextStyles.bodyMediumReg
|
||||
.copyWith(color: AppColors.blackGray)),
|
||||
Text(value, style: AppTextStyles.bodyMediumMed),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/features/create_event/presentation/create_role_section/create_role_widget.dart';
|
||||
import 'package:krow/features/create_event/presentation/create_shift_details_section/bloc/create_shift_details_bloc.dart';
|
||||
|
||||
class CreateRolesList extends StatefulWidget {
|
||||
final String id;
|
||||
|
||||
const CreateRolesList({super.key, required this.id});
|
||||
|
||||
@override
|
||||
State<CreateRolesList> createState() => _CreateRolesListState();
|
||||
}
|
||||
|
||||
class _CreateRolesListState extends State<CreateRolesList> {
|
||||
final Map<String, GlobalKey> roleKeys = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CreateShiftDetailsBloc, CreateShiftDetailsState>(
|
||||
builder: (context, state) {
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(0),
|
||||
shrinkWrap: true,
|
||||
primary: false,
|
||||
itemCount: state.roles.length,
|
||||
itemBuilder: (context, index) {
|
||||
final roleId = state.roles[index].id;
|
||||
if (roleKeys[roleId] == null) {
|
||||
roleKeys[roleId] = GlobalKey();
|
||||
}
|
||||
return BlocProvider.value(
|
||||
value: state.roles[index].bloc,
|
||||
child: CreateRoleDetailsWidget(
|
||||
key: roleKeys[roleId], id: state.roles[index].id),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:krow/core/data/models/event/business_member_model.dart';
|
||||
import 'package:krow/core/data/models/event/full_address_model.dart';
|
||||
import 'package:krow/core/entity/position_entity.dart';
|
||||
import 'package:krow/core/entity/shift_entity.dart';
|
||||
import 'package:krow/features/create_event/domain/google_places_service.dart';
|
||||
import 'package:krow/features/create_event/presentation/create_role_section/bloc/create_role_bloc.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
part 'create_shift_details_event.dart';
|
||||
part 'create_shift_details_state.dart';
|
||||
|
||||
class CreateShiftDetailsBloc
|
||||
extends Bloc<CreateShiftDetailsEvent, CreateShiftDetailsState> {
|
||||
CreateShiftDetailsBloc({required bool expanded})
|
||||
: super(CreateShiftDetailsState(
|
||||
suggestions: [],
|
||||
isExpanded: expanded,
|
||||
shift: ShiftEntity.empty(),
|
||||
roles: [
|
||||
RoleViewModel(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
bloc: CreateRoleBloc()),
|
||||
])) {
|
||||
on<CreateShiftInitializeEvent>(_onInitialize);
|
||||
on<CreateShiftAddressQueryChangedEvent>(_onQueryChanged);
|
||||
on<CreateShiftAddressSelectEvent>(_onAddressSelect);
|
||||
on<CreateShiftSelectContactEvent>(_onSelectContact);
|
||||
on<CreateShiftRemoveContactEvent>(_onRemoveContact);
|
||||
on<CreateEventAddRoleEvent>(_onAddRole);
|
||||
on<DeleteRoleDeleteEvent>(_onDeleteRole);
|
||||
on<ExpandShiftEvent>(_onExpandShift);
|
||||
on<ValidationFailedEvent>((event, emit) {
|
||||
emit(state.copyWith(validationState: event.validationState));
|
||||
});
|
||||
}
|
||||
|
||||
void _onInitialize(CreateShiftInitializeEvent event, emit) async {
|
||||
emit(CreateShiftDetailsState(
|
||||
suggestions: [],
|
||||
isExpanded: state.isExpanded,
|
||||
shift: event.shift,
|
||||
roles: [
|
||||
...event.shift.positions.map((roleEntity) => RoleViewModel(
|
||||
id: roleEntity.id,
|
||||
bloc: CreateRoleBloc()
|
||||
..add(
|
||||
CreateRoleInitEvent(roleEntity),
|
||||
))),
|
||||
]));
|
||||
}
|
||||
|
||||
void _onQueryChanged(CreateShiftAddressQueryChangedEvent event, emit) async {
|
||||
try {
|
||||
final googlePlacesService = GooglePlacesService();
|
||||
final suggestions =
|
||||
await googlePlacesService.fetchSuggestions(event.query);
|
||||
emit(state.copyWith(suggestions: suggestions));
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
|
||||
void _onAddressSelect(CreateShiftAddressSelectEvent event, emit) async {
|
||||
if (event.address != null) {
|
||||
emit(state.copyWith(
|
||||
suggestions: [],
|
||||
shift: state.shift.copyWith(fullAddress: event.address!)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.place != null) {
|
||||
final googlePlacesService = GooglePlacesService();
|
||||
final fullAddress =
|
||||
await googlePlacesService.getPlaceDetails(event.place!.placeId);
|
||||
FullAddress address = FullAddress.fromGoogle(fullAddress);
|
||||
emit(state.copyWith(
|
||||
suggestions: [], shift: state.shift.copyWith(fullAddress: address)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void _onSelectContact(CreateShiftSelectContactEvent event, emit) {
|
||||
emit(state.copyWith(
|
||||
shift: state.shift
|
||||
.copyWith(managers: [...state.shift.managers, event.contact])));
|
||||
}
|
||||
|
||||
void _onRemoveContact(CreateShiftRemoveContactEvent event, emit) {
|
||||
emit(state.copyWith(
|
||||
shift: state.shift.copyWith(
|
||||
managers: state.shift.managers
|
||||
.where((element) => element != event.contact)
|
||||
.toList())));
|
||||
}
|
||||
|
||||
void _onAddRole(CreateEventAddRoleEvent event, emit) {
|
||||
final id = DateTime.now().millisecondsSinceEpoch.toString();
|
||||
final bloc = CreateRoleBloc();
|
||||
|
||||
PositionEntity newPosition = PositionEntity.empty();
|
||||
|
||||
bloc.add(CreateRoleInitEvent(newPosition));
|
||||
|
||||
newPosition.parentShift = state.shift;
|
||||
state.shift.positions.add(newPosition);
|
||||
|
||||
emit(state.copyWith(
|
||||
roles: [
|
||||
...state.roles,
|
||||
RoleViewModel(id: id, bloc: bloc),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
void _onDeleteRole(DeleteRoleDeleteEvent event, emit) {
|
||||
emit(state.copyWith(
|
||||
roles: state.roles.where((element) => element.id != event.id).toList(),
|
||||
));
|
||||
}
|
||||
|
||||
void _onExpandShift(ExpandShiftEvent event, emit) {
|
||||
emit(state.copyWith(isExpanded: !state.isExpanded));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
part of 'create_shift_details_bloc.dart';
|
||||
|
||||
@immutable
|
||||
sealed class CreateShiftDetailsEvent {}
|
||||
|
||||
class CreateShiftInitializeEvent extends CreateShiftDetailsEvent {
|
||||
final ShiftEntity shift;
|
||||
|
||||
CreateShiftInitializeEvent(this.shift);
|
||||
}
|
||||
|
||||
class CreateShiftAddressSelectEvent extends CreateShiftDetailsEvent {
|
||||
final MapPlace? place;
|
||||
final FullAddress? address;
|
||||
|
||||
CreateShiftAddressSelectEvent({this.place, this.address});
|
||||
}
|
||||
|
||||
class CreateShiftAddressQueryChangedEvent extends CreateShiftDetailsEvent {
|
||||
final String query;
|
||||
|
||||
CreateShiftAddressQueryChangedEvent(this.query);
|
||||
}
|
||||
|
||||
class CreateShiftSelectContactEvent extends CreateShiftDetailsEvent {
|
||||
final BusinessMemberModel contact;
|
||||
|
||||
CreateShiftSelectContactEvent(this.contact);
|
||||
}
|
||||
|
||||
class CreateShiftRemoveContactEvent extends CreateShiftDetailsEvent {
|
||||
final BusinessMemberModel contact;
|
||||
|
||||
CreateShiftRemoveContactEvent(this.contact);
|
||||
}
|
||||
|
||||
class CreateEventAddRoleEvent extends CreateShiftDetailsEvent {
|
||||
CreateEventAddRoleEvent();
|
||||
}
|
||||
|
||||
class DeleteRoleDeleteEvent extends CreateShiftDetailsEvent {
|
||||
final String id;
|
||||
|
||||
DeleteRoleDeleteEvent(this.id);
|
||||
}
|
||||
|
||||
class ExpandShiftEvent extends CreateShiftDetailsEvent {
|
||||
ExpandShiftEvent();
|
||||
}
|
||||
|
||||
class ValidationFailedEvent extends CreateShiftDetailsEvent {
|
||||
final ShiftValidationState validationState;
|
||||
|
||||
ValidationFailedEvent(this.validationState);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
part of 'create_shift_details_bloc.dart';
|
||||
|
||||
@immutable
|
||||
class CreateShiftDetailsState {
|
||||
final bool inLoading;
|
||||
final bool isExpanded;
|
||||
final ShiftEntity shift;
|
||||
final List<MapPlace> suggestions;
|
||||
final List<RoleViewModel> roles;
|
||||
|
||||
final ShiftValidationState? validationState;
|
||||
|
||||
const CreateShiftDetailsState({
|
||||
required this.shift,
|
||||
this.inLoading = false,
|
||||
required this.suggestions,
|
||||
required this.roles,
|
||||
this.isExpanded = true,
|
||||
this.validationState
|
||||
});
|
||||
|
||||
CreateShiftDetailsState copyWith({
|
||||
bool? inLoading,
|
||||
List<MapPlace>? suggestions,
|
||||
ShiftEntity? shift,
|
||||
String? department,
|
||||
List<RoleViewModel>? roles,
|
||||
bool? isExpanded,
|
||||
ShiftValidationState? validationState,
|
||||
}) {
|
||||
return CreateShiftDetailsState(
|
||||
shift: shift ?? this.shift,
|
||||
inLoading: inLoading ?? false,
|
||||
suggestions: suggestions ?? this.suggestions,
|
||||
roles: roles ?? this.roles,
|
||||
isExpanded: isExpanded ?? this.isExpanded,
|
||||
validationState: validationState,
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RoleViewModel {
|
||||
final String id;
|
||||
final CreateRoleBloc bloc;
|
||||
|
||||
|
||||
RoleViewModel({required this.id, required this.bloc});
|
||||
}
|
||||
|
||||
class ShiftValidationState {
|
||||
|
||||
final String? addressError;
|
||||
final String? contactsError;
|
||||
bool showed;
|
||||
|
||||
bool get hasError =>
|
||||
addressError != null ||
|
||||
contactsError != null;
|
||||
|
||||
String? get message {
|
||||
return addressError ?? contactsError ?? '';
|
||||
}
|
||||
|
||||
ShiftValidationState({
|
||||
this.addressError,
|
||||
this.contactsError,this.showed = false});
|
||||
|
||||
ShiftValidationState copyWith({
|
||||
String? addressError,
|
||||
String? contactsError,
|
||||
}) {
|
||||
return ShiftValidationState(
|
||||
addressError: addressError ?? this.addressError,
|
||||
contactsError: contactsError ?? this.contactsError,
|
||||
showed: showed
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import 'package:expandable/expandable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_suggestion_input.dart';
|
||||
import 'package:krow/features/create_event/domain/bloc/create_event_bloc.dart';
|
||||
import 'package:krow/features/create_event/domain/google_places_service.dart';
|
||||
import 'package:krow/features/create_event/presentation/create_role_section/create_roles_list.dart';
|
||||
import 'package:krow/features/create_event/presentation/create_shift_details_section/bloc/create_shift_details_bloc.dart';
|
||||
import 'package:krow/features/create_event/presentation/create_shift_details_section/widgets/shift_contacts_widget.dart';
|
||||
|
||||
class CreateShiftDetailsWidget extends StatefulWidget {
|
||||
final String id;
|
||||
|
||||
final int index;
|
||||
|
||||
final bool expanded;
|
||||
|
||||
const CreateShiftDetailsWidget({
|
||||
super.key,
|
||||
required this.id,
|
||||
required this.index,
|
||||
this.expanded = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CreateShiftDetailsWidget> createState() =>
|
||||
_CreateShiftDetailsWidgetState();
|
||||
}
|
||||
|
||||
class _CreateShiftDetailsWidgetState extends State<CreateShiftDetailsWidget>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
ExpandableController? _expandableController;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_expandableController =
|
||||
ExpandableController(initialExpanded: widget.expanded);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_expandableController?.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return BlocBuilder<CreateEventBloc, CreateEventState>(
|
||||
builder: (context, eventState) {
|
||||
return BlocConsumer<CreateShiftDetailsBloc, CreateShiftDetailsState>(
|
||||
listener: (context, state) {
|
||||
if (state.isExpanded != _expandableController?.expanded) {
|
||||
_expandableController!.toggle();
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.only(top: 12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: KwBoxDecorations.white12.copyWith(
|
||||
border: state.validationState != null
|
||||
? Border.all(color: AppColors.statusError, width: 1)
|
||||
: null),
|
||||
child: ExpandableTheme(
|
||||
data: const ExpandableThemeData(
|
||||
hasIcon: false,
|
||||
animationDuration: Duration(milliseconds: 250)),
|
||||
child: ExpandablePanel(
|
||||
collapsed: Container(),
|
||||
controller: _expandableController,
|
||||
header: _buildHeader(
|
||||
context, state, eventState.shifts.length > 1),
|
||||
expanded: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_addressInput(state, context),
|
||||
ShiftContactsWidget(
|
||||
allContacts: eventState.contacts,
|
||||
selectedContacts: state.shift?.managers ?? []),
|
||||
CreateRolesList(id: widget.id),
|
||||
const Gap(12),
|
||||
KwButton.outlinedPrimary(
|
||||
label: 'Add New Role',
|
||||
leftIcon: Assets.images.icons.add,
|
||||
onPressed: () {
|
||||
context
|
||||
.read<CreateShiftDetailsBloc>()
|
||||
.add(CreateEventAddRoleEvent());
|
||||
}),
|
||||
const Gap(12),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(
|
||||
BuildContext context,
|
||||
CreateShiftDetailsState state,
|
||||
bool canBeRemoved,
|
||||
) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
context.read<CreateShiftDetailsBloc>().add(ExpandShiftEvent());
|
||||
},
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Shift Details #${widget.index + 1}',
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
),
|
||||
if (!state.isExpanded)
|
||||
Center(
|
||||
child: Assets.images.icons.chevronDown.svg(
|
||||
height: 24,
|
||||
width: 24,
|
||||
colorFilter: const ColorFilter.mode(
|
||||
AppColors.grayStroke,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (state.isExpanded && canBeRemoved)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context
|
||||
.read<CreateEventBloc>()
|
||||
.add(CreateEventRemoveShift(widget.id));
|
||||
},
|
||||
child: Container(
|
||||
height: 24,
|
||||
width: 24,
|
||||
color: Colors.transparent,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 4),
|
||||
child: Center(
|
||||
child: Assets.images.icons.delete.svg(
|
||||
width: 16,
|
||||
height: 16,
|
||||
colorFilter: const ColorFilter.mode(
|
||||
AppColors.statusError, BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
KwSuggestionInput<MapPlace> _addressInput(
|
||||
CreateShiftDetailsState state, BuildContext context) {
|
||||
state.suggestions.removeWhere(
|
||||
(e) => e.description == state.shift.fullAddress?.formattedAddress);
|
||||
return KwSuggestionInput(
|
||||
key: ValueKey('address_key${widget.id}'),
|
||||
title: 'Address',
|
||||
hintText: 'Hub address',
|
||||
horizontalPadding: 28,
|
||||
initialText: state.shift.fullAddress?.formattedAddress,
|
||||
items: state.suggestions,
|
||||
onQueryChanged: (query) {
|
||||
context
|
||||
.read<CreateShiftDetailsBloc>()
|
||||
.add(CreateShiftAddressQueryChangedEvent(query));
|
||||
},
|
||||
itemToStringBuilder: (item) => item.description,
|
||||
onSelected: (item) {
|
||||
context
|
||||
.read<CreateShiftDetailsBloc>()
|
||||
.add(CreateShiftAddressSelectEvent(place: item));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/features/create_event/domain/bloc/create_event_bloc.dart';
|
||||
import 'package:krow/features/create_event/presentation/create_shift_details_section/create_shift_widget.dart';
|
||||
|
||||
class CreateShiftsList extends StatefulWidget {
|
||||
const CreateShiftsList({super.key});
|
||||
|
||||
@override
|
||||
State<CreateShiftsList> createState() => _CreateShiftsListState();
|
||||
}
|
||||
|
||||
class _CreateShiftsListState extends State<CreateShiftsList> {
|
||||
final Map<String, GlobalKey> shiftKeys = {};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CreateEventBloc, CreateEventState>(
|
||||
builder: (context, state) {
|
||||
return SliverList.builder(
|
||||
itemCount: state.shifts.length,
|
||||
itemBuilder: (context, index) {
|
||||
final shiftId = state.shifts[index].id;
|
||||
if (shiftKeys[shiftId] == null) {
|
||||
shiftKeys[shiftId] = GlobalKey();
|
||||
}
|
||||
return BlocProvider.value(
|
||||
value: state.shifts[index].bloc,
|
||||
child: CreateShiftDetailsWidget(
|
||||
key: shiftKeys[shiftId],
|
||||
id: state.shifts[index].id,
|
||||
expanded: state.shifts[index].bloc.state.isExpanded,
|
||||
index: index),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/data/models/event/business_member_model.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
|
||||
class SelectContactPopup extends StatefulWidget {
|
||||
final List<BusinessMemberModel> contacts;
|
||||
final void Function(BusinessMemberModel contact)? onSelect;
|
||||
final Widget child;
|
||||
|
||||
const SelectContactPopup(
|
||||
{super.key,
|
||||
required this.contacts,
|
||||
required this.onSelect,
|
||||
required this.child});
|
||||
|
||||
@override
|
||||
_SelectContactPopupState createState() => _SelectContactPopupState();
|
||||
}
|
||||
|
||||
class _SelectContactPopupState extends State<SelectContactPopup> {
|
||||
OverlayEntry? _overlayEntry;
|
||||
List<BusinessMemberModel> _filteredItems = [];
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
final TextEditingController _controller = TextEditingController();
|
||||
final GlobalKey _childKey = GlobalKey();
|
||||
double? childY;
|
||||
|
||||
StateSetter? overlaySetState;
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _showPopup(BuildContext context) {
|
||||
_filteredItems = List.from(widget.contacts);
|
||||
|
||||
_overlayEntry = OverlayEntry(
|
||||
builder: (context) {
|
||||
return StatefulBuilder(builder: (context, setState) {
|
||||
overlaySetState = setState;
|
||||
return Stack(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: _hidePopup,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(color: Colors.transparent),
|
||||
),
|
||||
if (childY != null)
|
||||
Positioned(
|
||||
height:
|
||||
min(320, 82 + (44 * _filteredItems.length).toDouble()),
|
||||
top: childY,
|
||||
left: 28,
|
||||
right: 28,
|
||||
child: Material(
|
||||
elevation: 4,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: _buildPopupContent(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
Overlay.of(context).insert(_overlayEntry!);
|
||||
}
|
||||
|
||||
Widget _buildPopupContent() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: AppColors.grayTintStroke),
|
||||
),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
height: 40,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.grayPrimaryFrame,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Assets.images.icons.magnifyingGlass.svg(),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
style: AppTextStyles.bodyLargeMed,
|
||||
decoration: const InputDecoration(
|
||||
contentPadding: EdgeInsets.only(bottom: 8),
|
||||
hintText: 'Search',
|
||||
hintStyle: AppTextStyles.bodyLargeMed,
|
||||
border: InputBorder.none,
|
||||
),
|
||||
onChanged: _filterItems,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
height: min(238, (44 * _filteredItems.length).toDouble()),
|
||||
child: RawScrollbar(
|
||||
controller: _scrollController,
|
||||
thumbVisibility: true,
|
||||
padding: const EdgeInsets.only(right: 4),
|
||||
thumbColor: AppColors.grayDisable,
|
||||
trackColor: AppColors.buttonTertiaryActive,
|
||||
trackVisibility: true,
|
||||
radius: const Radius.circular(20),
|
||||
thickness: 4,
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
itemCount: _filteredItems.length,
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
itemBuilder: (context, index) {
|
||||
var item = _filteredItems[index];
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
widget.onSelect?.call(item);
|
||||
_hidePopup();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'${item.firstName} ${item.lastName}',
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
item.authInfo?.phone ?? '',
|
||||
style: AppTextStyles.bodySmallMed
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
final RenderBox renderBox =
|
||||
_childKey.currentContext!.findRenderObject() as RenderBox;
|
||||
final position = renderBox.localToGlobal(Offset.zero);
|
||||
childY = position.dy;
|
||||
_showPopup(context);
|
||||
},
|
||||
key: _childKey,
|
||||
child: widget.child);
|
||||
}
|
||||
|
||||
void _hidePopup() {
|
||||
_overlayEntry?.remove();
|
||||
_overlayEntry = null;
|
||||
_filteredItems = List.from(widget.contacts);
|
||||
_controller.clear();
|
||||
}
|
||||
|
||||
void _filterItems(String query) {
|
||||
overlaySetState?.call(() {
|
||||
_filteredItems = widget.contacts.where((item) {
|
||||
return ('${item.firstName}${item.lastName}${item.authInfo?.phone ?? ''}')
|
||||
.toLowerCase()
|
||||
.contains(query.toLowerCase());
|
||||
}).toList();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/data/models/event/business_member_model.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/features/create_event/presentation/create_shift_details_section/bloc/create_shift_details_bloc.dart';
|
||||
import 'package:krow/features/create_event/presentation/create_shift_details_section/widgets/select_contact_popup.dart';
|
||||
|
||||
class ShiftContactsWidget extends StatelessWidget {
|
||||
final List<BusinessMemberModel> allContacts;
|
||||
final List<BusinessMemberModel> selectedContacts;
|
||||
|
||||
const ShiftContactsWidget(
|
||||
{super.key, required this.allContacts, required this.selectedContacts});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
if (selectedContacts.isEmpty) {
|
||||
return buildSelectContactPopup(context);
|
||||
}
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(top: 12, left: 12, right: 12),
|
||||
margin: const EdgeInsets.only(top: 12),
|
||||
decoration: KwBoxDecorations.primaryLight12,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Shift Contact',
|
||||
style: AppTextStyles.bodyTinyReg
|
||||
.copyWith(color: AppColors.blackGray)),
|
||||
for (var item in selectedContacts)
|
||||
_buildContactItem(context, item, selectedContacts),
|
||||
buildSelectContactPopup(context),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
Widget buildSelectContactPopup(BuildContext context) {
|
||||
return SelectContactPopup(
|
||||
key: ObjectKey(selectedContacts),
|
||||
contacts: allContacts
|
||||
.where((element) => !selectedContacts.contains(element))
|
||||
.toList(),
|
||||
onSelect: (contact) {
|
||||
BlocProvider.of<CreateShiftDetailsBloc>(context)
|
||||
.add(CreateShiftSelectContactEvent(contact));
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 12, bottom: 12),
|
||||
height: 20,
|
||||
child: Row(
|
||||
children: [
|
||||
Assets.images.icons.profileAdd.svg(),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'Shift Contact',
|
||||
style: AppTextStyles.bodyMediumMed
|
||||
.copyWith(color: AppColors.primaryBlue),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Container _buildContactItem(BuildContext context, BusinessMemberModel item,
|
||||
List<BusinessMemberModel> selectedContacts) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
height: 36,
|
||||
width: 36,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(18), color: Colors.grey),
|
||||
),
|
||||
const Gap(8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${item.firstName} ${item.lastName}',
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
),
|
||||
const Gap(2),
|
||||
Text(
|
||||
item.authInfo?.phone ?? '',
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
SelectContactPopup(
|
||||
key: ObjectKey(selectedContacts),
|
||||
contacts: allContacts
|
||||
.where((element) => !selectedContacts.contains(element))
|
||||
.toList(),
|
||||
onSelect: (contact) {
|
||||
BlocProvider.of<CreateShiftDetailsBloc>(context)
|
||||
.add(CreateShiftRemoveContactEvent(item));
|
||||
|
||||
BlocProvider.of<CreateShiftDetailsBloc>(context)
|
||||
.add(CreateShiftSelectContactEvent(contact));
|
||||
},
|
||||
child: Container(
|
||||
height: 34,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.grayWhite,
|
||||
borderRadius: BorderRadius.circular(17),
|
||||
border: Border.all(color: AppColors.grayTintStroke)),
|
||||
child: Row(
|
||||
children: [
|
||||
Center(
|
||||
child: Assets.images.icons.edit.svg(
|
||||
width: 16,
|
||||
height: 16,
|
||||
colorFilter: const ColorFilter.mode(
|
||||
AppColors.blackBlack, BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
const Text(
|
||||
'Edit',
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
BlocProvider.of<CreateShiftDetailsBloc>(context)
|
||||
.add(CreateShiftRemoveContactEvent(item));
|
||||
},
|
||||
child: Container(
|
||||
height: 34,
|
||||
width: 34,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.grayWhite,
|
||||
borderRadius: BorderRadius.circular(17),
|
||||
border: Border.all(color: AppColors.grayTintStroke)),
|
||||
child: Center(
|
||||
child: Assets.images.icons.delete.svg(
|
||||
width: 16,
|
||||
height: 16,
|
||||
colorFilter: const ColorFilter.mode(
|
||||
AppColors.statusError, BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:krow/core/data/models/event/event_model.dart';
|
||||
import 'package:krow/core/entity/event_entity.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
part 'date_selector_event.dart';
|
||||
part 'date_selector_state.dart';
|
||||
|
||||
class DateSelectorBloc extends Bloc<DateSelectorEvent, DateSelectorState> {
|
||||
DateSelectorBloc() : super(const DateSelectorState()) {
|
||||
on<DateSelectorEventInit>(_onInit);
|
||||
on<DateSelectorEventChangeStartDate>(_onChangeStartDate);
|
||||
on<DateSelectorEventChangeEndDate>(_onChangeEndDate);
|
||||
on<DateSelectorEventRepeatType>(_onChangeRepeatType);
|
||||
on<DateSelectorEventEndType>(_onChangeEndType);
|
||||
on<DateSelectorEventChangeEndsAfterWeeks>(_onChangeEndsAfterWeeks);
|
||||
}
|
||||
|
||||
void _onInit(DateSelectorEventInit event, Emitter<DateSelectorState> emit) {
|
||||
if (event.eventModel != null) {
|
||||
emit(state.copyWith(
|
||||
startDate: DateTime.parse(event.eventModel!.date),
|
||||
));
|
||||
return;
|
||||
} else {
|
||||
emit(state.copyWith(
|
||||
recurringType: event.recurringType,
|
||||
endDateType: EndDateType.endDate,
|
||||
repeatType: EventRepeatType.daily,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void _onChangeStartDate(
|
||||
DateSelectorEventChangeStartDate event, Emitter<DateSelectorState> emit) {
|
||||
if (event.startDate == state.startDate) return;
|
||||
|
||||
event.entity.startDate = event.startDate;
|
||||
|
||||
emit(state.copyWith(
|
||||
startDate: event.startDate,
|
||||
));
|
||||
}
|
||||
|
||||
void _onChangeEndDate(
|
||||
DateSelectorEventChangeEndDate event, Emitter<DateSelectorState> emit) {
|
||||
if (event.endDate == state.startDate) return;
|
||||
event.entity.endDate = event.endDate;
|
||||
|
||||
emit(state.copyWith(
|
||||
endDate: event.endDate,
|
||||
));
|
||||
}
|
||||
|
||||
void _onChangeRepeatType(
|
||||
DateSelectorEventRepeatType event, Emitter<DateSelectorState> emit) {
|
||||
if (event.type == state.repeatType) return;
|
||||
emit(state.copyWith(
|
||||
repeatType: event.type,
|
||||
));
|
||||
}
|
||||
|
||||
void _onChangeEndType(
|
||||
DateSelectorEventEndType event, Emitter<DateSelectorState> emit) {
|
||||
if (event.type == state.endDateType) return;
|
||||
emit(state.copyWith(
|
||||
endDateType: event.type,
|
||||
));
|
||||
}
|
||||
|
||||
void _onChangeEndsAfterWeeks(DateSelectorEventChangeEndsAfterWeeks event,
|
||||
Emitter<DateSelectorState> emit) {
|
||||
if (event.weeks == state.andsAfterWeeksCount) return;
|
||||
emit(state.copyWith(
|
||||
andsAfterWeeksCount: event.weeks,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
part of 'date_selector_bloc.dart';
|
||||
|
||||
@immutable
|
||||
sealed class DateSelectorEvent {}
|
||||
|
||||
class DateSelectorEventInit extends DateSelectorEvent {
|
||||
final EventScheduleType? recurringType;
|
||||
final EventModel? eventModel;
|
||||
|
||||
DateSelectorEventInit(this.recurringType, this.eventModel);
|
||||
}
|
||||
|
||||
class DateSelectorEventChangeStartDate extends DateSelectorEvent {
|
||||
final DateTime startDate;
|
||||
final EventEntity entity;
|
||||
|
||||
DateSelectorEventChangeStartDate(this.startDate, this.entity);
|
||||
}
|
||||
|
||||
class DateSelectorEventChangeEndDate extends DateSelectorEvent {
|
||||
final DateTime endDate;
|
||||
final EventEntity entity;
|
||||
|
||||
|
||||
DateSelectorEventChangeEndDate(this.endDate, this.entity);
|
||||
}
|
||||
|
||||
class DateSelectorEventRepeatType extends DateSelectorEvent {
|
||||
final EventRepeatType type;
|
||||
|
||||
DateSelectorEventRepeatType(this.type);
|
||||
}
|
||||
|
||||
class DateSelectorEventEndType extends DateSelectorEvent {
|
||||
final EndDateType type;
|
||||
|
||||
DateSelectorEventEndType(this.type);
|
||||
}
|
||||
|
||||
class DateSelectorEventChangeEndsAfterWeeks extends DateSelectorEvent {
|
||||
final int weeks;
|
||||
|
||||
DateSelectorEventChangeEndsAfterWeeks(this.weeks);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
part of 'date_selector_bloc.dart';
|
||||
|
||||
enum EndDateType { endDate, endsAfter }
|
||||
|
||||
enum EventRepeatType {
|
||||
weekly,
|
||||
daily,
|
||||
}
|
||||
|
||||
@immutable
|
||||
class DateSelectorState {
|
||||
final DateTime? startDate;
|
||||
final DateTime? endDate;
|
||||
final EventScheduleType? recurringType;
|
||||
final EndDateType? endDateType;
|
||||
final EventRepeatType? repeatType;
|
||||
final int andsAfterWeeksCount;
|
||||
|
||||
const DateSelectorState(
|
||||
{this.startDate,
|
||||
this.endDate,
|
||||
this.recurringType = EventScheduleType.oneTime,
|
||||
this.endDateType,
|
||||
this.repeatType,
|
||||
this.andsAfterWeeksCount = 1});
|
||||
|
||||
DateSelectorState copyWith({
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
EventScheduleType? recurringType,
|
||||
EndDateType? endDateType,
|
||||
EventRepeatType? repeatType,
|
||||
int? andsAfterWeeksCount,
|
||||
}) {
|
||||
return DateSelectorState(
|
||||
startDate: startDate ?? this.startDate,
|
||||
endDate: endDate ?? this.endDate,
|
||||
recurringType: recurringType ?? this.recurringType,
|
||||
endDateType: endDateType ?? this.endDateType,
|
||||
repeatType: repeatType ?? this.repeatType,
|
||||
andsAfterWeeksCount: andsAfterWeeksCount ?? this.andsAfterWeeksCount,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
import 'package:calendar_date_picker2/calendar_date_picker2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
|
||||
class CreateEventCalendar extends StatefulWidget {
|
||||
final DateTime? initialDate;
|
||||
|
||||
final void Function(DateTime value) onDateSelected;
|
||||
|
||||
const CreateEventCalendar(
|
||||
{super.key, required this.initialDate, required this.onDateSelected});
|
||||
|
||||
@override
|
||||
State<CreateEventCalendar> createState() => _CreateEventCalendarState();
|
||||
}
|
||||
|
||||
class _CreateEventCalendarState extends State<CreateEventCalendar> {
|
||||
final monthStr = [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec'
|
||||
];
|
||||
|
||||
final selectedTextStyle = AppTextStyles.bodyMediumSmb.copyWith(
|
||||
color: AppColors.grayWhite,
|
||||
);
|
||||
|
||||
final dayTextStyle = AppTextStyles.bodyMediumReg.copyWith(
|
||||
color: AppColors.bgColorDark,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CalendarDatePicker2(
|
||||
value: widget.initialDate == null ? [] : [widget.initialDate],
|
||||
config: CalendarDatePicker2Config(
|
||||
hideMonthPickerDividers: false,
|
||||
modePickersGap: 0,
|
||||
controlsHeight: 80,
|
||||
calendarType: CalendarDatePicker2Type.single,
|
||||
selectedRangeHighlightColor: AppColors.tintGray,
|
||||
selectedDayTextStyle: selectedTextStyle,
|
||||
dayTextStyle: dayTextStyle,
|
||||
selectedMonthTextStyle: selectedTextStyle,
|
||||
selectedYearTextStyle: selectedTextStyle,
|
||||
selectedDayHighlightColor: AppColors.bgColorDark,
|
||||
centerAlignModePicker: true,
|
||||
monthTextStyle: dayTextStyle,
|
||||
weekdayLabelBuilder: _dayWeekBuilder,
|
||||
dayBuilder: _dayBuilder,
|
||||
monthBuilder: _monthBuilder,
|
||||
yearBuilder: _yearBuilder,
|
||||
controlsTextStyle: AppTextStyles.headingH3,
|
||||
nextMonthIcon: Assets.images.icons.caretRight.svg(width: 24),
|
||||
lastMonthIcon: Assets.images.icons.caretLeft.svg(width: 24),
|
||||
customModePickerIcon: Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: Assets.images.icons.caretDown.svg(),
|
||||
),
|
||||
|
||||
// modePickerBuilder: _controlBuilder,
|
||||
),
|
||||
onValueChanged: (dates) {
|
||||
widget.onDateSelected.call(dates.first);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _monthBuilder({
|
||||
required int month,
|
||||
TextStyle? textStyle,
|
||||
BoxDecoration? decoration,
|
||||
bool? isSelected,
|
||||
bool? isDisabled,
|
||||
bool? isCurrentMonth,
|
||||
}) {
|
||||
return Center(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 16),
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected == true ? AppColors.bgColorDark : null,
|
||||
borderRadius: BorderRadius.circular(23),
|
||||
border: Border.all(
|
||||
color: AppColors.grayStroke,
|
||||
width: isSelected == true ? 0 : 1,
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
monthStr[month - 1],
|
||||
style: isSelected == true
|
||||
? AppTextStyles.bodyMediumMed
|
||||
.copyWith(color: AppColors.grayWhite)
|
||||
: AppTextStyles.bodyMediumReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _yearBuilder({
|
||||
required int year,
|
||||
TextStyle? textStyle,
|
||||
BoxDecoration? decoration,
|
||||
bool? isSelected,
|
||||
bool? isDisabled,
|
||||
bool? isCurrentYear,
|
||||
}) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 12),
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected == true ? AppColors.bgColorDark : null,
|
||||
borderRadius: BorderRadius.circular(23),
|
||||
border: Border.all(
|
||||
color: AppColors.grayStroke,
|
||||
width: isSelected == true ? 0 : 1,
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
year.toString(),
|
||||
style: isSelected == true
|
||||
? AppTextStyles.bodyMediumMed.copyWith(color: AppColors.grayWhite)
|
||||
: AppTextStyles.bodyMediumReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _dayBuilder({
|
||||
required DateTime date,
|
||||
TextStyle? textStyle,
|
||||
BoxDecoration? decoration,
|
||||
bool? isSelected,
|
||||
bool? isDisabled,
|
||||
bool? isToday,
|
||||
}) {
|
||||
bool past = _isPast(date);
|
||||
var dayDecoration = BoxDecoration(
|
||||
color: isSelected == true ? AppColors.bgColorDark : null,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
);
|
||||
var dayTextStyle = AppTextStyles.bodyMediumReg.copyWith(
|
||||
color: past ? AppColors.blackCaptionText : AppColors.bgColorDark,
|
||||
);
|
||||
return Center(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(left: 2, right: 2),
|
||||
alignment: Alignment.center,
|
||||
decoration: dayDecoration,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
date.day.toString(),
|
||||
style: isSelected == true ? selectedTextStyle : dayTextStyle,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const Gap(2),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool _isPast(DateTime date) {
|
||||
var now = DateTime.now();
|
||||
var nowOnly = DateTime(now.year, now.month, now.day);
|
||||
var past = date.isBefore(nowOnly);
|
||||
return past;
|
||||
}
|
||||
|
||||
Widget? _dayWeekBuilder({
|
||||
required int weekday,
|
||||
bool? isScrollViewTopHeader,
|
||||
}) {
|
||||
return Text(
|
||||
['S', 'M', 'T', 'W', 'T', 'F', 'S'][weekday],
|
||||
style: AppTextStyles.bodyMediumSmb.copyWith(
|
||||
color: AppColors.bgColorDark,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
|
||||
import 'package:krow/features/create_event/presentation/event_date_section/create_event_calendar_widget.dart';
|
||||
|
||||
class CreateEventDatePopup extends StatefulWidget {
|
||||
final DateTime? initDate;
|
||||
|
||||
const CreateEventDatePopup({
|
||||
super.key,
|
||||
this.initDate,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CreateEventDatePopup> createState() => _CreateEventDatePopupState();
|
||||
|
||||
static Future<DateTime?> show(
|
||||
{required BuildContext context, DateTime? initDate}) async {
|
||||
return showDialog<DateTime>(
|
||||
context: context,
|
||||
builder: (context) => CreateEventDatePopup(
|
||||
initDate: initDate,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CreateEventDatePopupState extends State<CreateEventDatePopup> {
|
||||
DateTime? selectedDate;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
selectedDate = widget.initDate;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
margin: const EdgeInsets.all(16),
|
||||
decoration: KwBoxDecorations.white24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
buildHeader(),
|
||||
const Gap(24),
|
||||
_buildCalendar(),
|
||||
const Gap(24),
|
||||
..._buttonGroup(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Row buildHeader() {
|
||||
return const Row(
|
||||
children: [
|
||||
Text('Select Date from Calendar', style: AppTextStyles.headingH3),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buttonGroup() {
|
||||
return [
|
||||
KwButton.primary(
|
||||
disabled: selectedDate == null,
|
||||
label: 'Pick Date',
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(selectedDate);
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
KwButton.outlinedPrimary(
|
||||
label: 'Cancel',
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(widget.initDate);
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
_buildCalendar() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.grayPrimaryFrame,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColors.grayTintStroke),
|
||||
),
|
||||
child: CreateEventCalendar(
|
||||
onDateSelected: (date) {
|
||||
setState(() {
|
||||
selectedDate = date;
|
||||
});
|
||||
},
|
||||
initialDate: selectedDate ?? widget.initDate,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow/core/data/models/event/event_model.dart';
|
||||
import 'package:krow/core/entity/event_entity.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_dropdown.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_option_selector.dart';
|
||||
import 'package:krow/features/create_event/domain/bloc/create_event_bloc.dart';
|
||||
import 'package:krow/features/create_event/presentation/event_date_section/bloc/date_selector_bloc.dart';
|
||||
import 'package:krow/features/create_event/presentation/event_date_section/create_event_date_popup.dart';
|
||||
|
||||
class EventDateInputWidget extends StatelessWidget {
|
||||
final EventEntity entity;
|
||||
|
||||
const EventDateInputWidget(this.entity, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<DateSelectorBloc, DateSelectorState>(
|
||||
listener: (context, state) {
|
||||
BlocProvider.of<CreateEventBloc>(context)
|
||||
.add(CreateEventEntityUpdatedEvent());
|
||||
},
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: state.recurringType == EventScheduleType.oneTime
|
||||
? ([
|
||||
_buildDateInput(context, 'Date', state.startDate, (newDate) {
|
||||
BlocProvider.of<DateSelectorBloc>(context)
|
||||
.add(DateSelectorEventChangeStartDate(newDate, entity));
|
||||
}),
|
||||
])
|
||||
: [
|
||||
const Gap(16),
|
||||
const Divider(
|
||||
color: AppColors.grayTintStroke,
|
||||
thickness: 1,
|
||||
height: 0,
|
||||
),
|
||||
_buildDateInput(context, 'Start Date', state.startDate,
|
||||
(newDate) {
|
||||
BlocProvider.of<DateSelectorBloc>(context)
|
||||
.add(DateSelectorEventChangeStartDate(newDate, entity));
|
||||
}),
|
||||
..._buildRepeatSelector(state, context),
|
||||
...buildEndTypeSelector(state, context),
|
||||
AnimatedCrossFade(
|
||||
firstChild: _buildDateInput(
|
||||
context, 'End Date', state.endDate, (newDate) {
|
||||
BlocProvider.of<DateSelectorBloc>(context)
|
||||
.add(DateSelectorEventChangeEndDate(newDate, entity));
|
||||
}),
|
||||
secondChild: _buildEndAfterDropdown(state, context),
|
||||
crossFadeState: state.endDateType == EndDateType.endDate
|
||||
? CrossFadeState.showFirst
|
||||
: CrossFadeState.showSecond,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
),
|
||||
const Gap(16),
|
||||
const Divider(
|
||||
color: AppColors.grayTintStroke,
|
||||
thickness: 1,
|
||||
height: 0,
|
||||
),
|
||||
const Gap(8),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEndAfterDropdown(DateSelectorState state, BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: KwDropdown<int>(
|
||||
selectedItem: KwDropDownItem(
|
||||
data: state.andsAfterWeeksCount,
|
||||
title: '${state.andsAfterWeeksCount} Weeks'),
|
||||
hintText: '',
|
||||
title: 'After',
|
||||
horizontalPadding: 28,
|
||||
onSelected: (int item) {
|
||||
BlocProvider.of<DateSelectorBloc>(context)
|
||||
.add(DateSelectorEventChangeEndsAfterWeeks(item));
|
||||
},
|
||||
items: const [
|
||||
KwDropDownItem(data: 1, title: '1 Weeks'),
|
||||
KwDropDownItem(data: 2, title: '2 Weeks'),
|
||||
KwDropDownItem(data: 3, title: '3 Weeks'),
|
||||
KwDropDownItem(data: 5, title: '4 Weeks'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildRepeatSelector(
|
||||
DateSelectorState state, BuildContext context) {
|
||||
return [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16, top: 8, bottom: 4),
|
||||
child: Text(
|
||||
'Repeat',
|
||||
style: AppTextStyles.bodyTinyReg.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
),
|
||||
KwOptionSelector(
|
||||
selectedIndex: state.repeatType?.index,
|
||||
backgroundColor: AppColors.grayPrimaryFrame,
|
||||
onChanged: (index) {
|
||||
BlocProvider.of<DateSelectorBloc>(context).add(
|
||||
DateSelectorEventRepeatType(EventRepeatType.values[index]));
|
||||
},
|
||||
items: const [
|
||||
'Weekly',
|
||||
'Daily',
|
||||
])
|
||||
];
|
||||
}
|
||||
|
||||
Widget _buildDateInput(BuildContext context, String title, DateTime? date,
|
||||
Function(DateTime date) onSelect) {
|
||||
var formattedDate = date == null
|
||||
? 'mm.dd.yyyy'
|
||||
: DateFormat('MM.dd.yyyy').format(date).toLowerCase();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16, top: 8),
|
||||
child: Text(
|
||||
'Date',
|
||||
style:
|
||||
AppTextStyles.bodyTinyReg.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
var newDate = await CreateEventDatePopup.show(
|
||||
context: context, initDate: date);
|
||||
if (newDate != null && context.mounted) {
|
||||
onSelect(newDate);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
height: 48,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: AppColors.grayStroke),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(formattedDate,
|
||||
style: AppTextStyles.bodyMediumReg.copyWith(
|
||||
color: date != null ? null : AppColors.blackGray)),
|
||||
const Gap(10),
|
||||
Assets.images.icons.calendar.svg(width: 16, height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> buildEndTypeSelector(
|
||||
DateSelectorState state, BuildContext context) {
|
||||
return [
|
||||
const Gap(24),
|
||||
KwOptionSelector(
|
||||
selectedIndex: state.endDateType?.index,
|
||||
onChanged: (index) {
|
||||
BlocProvider.of<DateSelectorBloc>(context)
|
||||
.add(DateSelectorEventEndType(EndDateType.values[index]));
|
||||
},
|
||||
height: 26,
|
||||
selectorHeight: 4,
|
||||
textStyle:
|
||||
AppTextStyles.bodyMediumReg.copyWith(color: AppColors.blackGray),
|
||||
selectedTextStyle: AppTextStyles.bodyMediumMed,
|
||||
itemAlign: Alignment.topCenter,
|
||||
items: const [
|
||||
'Ends on Date',
|
||||
'Ends After # Weeks',
|
||||
])
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:krow/core/entity/role_schedule_entity.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
part 'role_schedule_dialog_event.dart';
|
||||
part 'role_schedule_dialog_state.dart';
|
||||
|
||||
class RoleScheduleDialogBloc
|
||||
extends Bloc<RoleScheduleDialogEvent, RoleScheduleDialogState> {
|
||||
RoleScheduleDialogBloc(List<RoleScheduleEntity> schedule)
|
||||
: super(RoleScheduleDialogState(schedule: List.from(schedule))) {
|
||||
on<TapOnScheduleDayEvent>(_onTapOnScheduleDay);
|
||||
on<SetRoleScheduleStartTimeEvent>(_onSetRoleScheduleStartTime);
|
||||
on<SetRoleScheduleEndTimeEvent>(_onSetRoleScheduleEndTime);
|
||||
}
|
||||
|
||||
FutureOr<void> _onTapOnScheduleDay(TapOnScheduleDayEvent event, emit) {
|
||||
if (state.schedule.any((element) => element.dayIndex == event.index)) {
|
||||
emit(state.copyWith(
|
||||
schedule: state.schedule
|
||||
.where((element) => element.dayIndex != event.index)
|
||||
.toList()));
|
||||
} else {
|
||||
var today = DateTime.now();
|
||||
var defStartTime = DateTime(today.year, today.month, today.day, 9, 0);
|
||||
var defEndTime = DateTime(today.year, today.month, today.day, 18, 0);
|
||||
emit(state.copyWith(schedule: [
|
||||
...state.schedule,
|
||||
RoleScheduleEntity(
|
||||
dayIndex: event.index, startTime: defStartTime, endTime: defEndTime)
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
FutureOr<void> _onSetRoleScheduleStartTime(event, emit) {
|
||||
emit(state.copyWith(
|
||||
schedule: state.schedule
|
||||
.map((e) => e.dayIndex == event.schedule.dayIndex
|
||||
? e.copyWith(startTime: event.startTime)
|
||||
: e)
|
||||
.toList()));
|
||||
}
|
||||
|
||||
FutureOr<void> _onSetRoleScheduleEndTime(event, emit) {
|
||||
emit(state.copyWith(
|
||||
schedule: state.schedule
|
||||
.map((e) => e.dayIndex == event.schedule.dayIndex
|
||||
? e.copyWith(endTime: event.endTime)
|
||||
: e)
|
||||
.toList()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
part of 'role_schedule_dialog_bloc.dart';
|
||||
|
||||
@immutable
|
||||
sealed class RoleScheduleDialogEvent {}
|
||||
|
||||
class TapOnScheduleDayEvent extends RoleScheduleDialogEvent {
|
||||
final int index;
|
||||
|
||||
TapOnScheduleDayEvent(this.index);
|
||||
}
|
||||
|
||||
class SetRoleScheduleStartTimeEvent extends RoleScheduleDialogEvent {
|
||||
final RoleScheduleEntity schedule;
|
||||
final DateTime startTime;
|
||||
|
||||
SetRoleScheduleStartTimeEvent(this.schedule, this.startTime);
|
||||
}
|
||||
|
||||
class SetRoleScheduleEndTimeEvent extends RoleScheduleDialogEvent {
|
||||
final RoleScheduleEntity schedule;
|
||||
final DateTime endTime;
|
||||
|
||||
SetRoleScheduleEndTimeEvent(this.schedule, this.endTime);
|
||||
}
|
||||
|
||||
class SaveRoleScheduleEvent extends RoleScheduleDialogEvent {
|
||||
SaveRoleScheduleEvent();
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
part of 'role_schedule_dialog_bloc.dart';
|
||||
|
||||
@immutable
|
||||
class RoleScheduleDialogState {
|
||||
final List<RoleScheduleEntity> schedule;
|
||||
|
||||
const RoleScheduleDialogState({required this.schedule});
|
||||
|
||||
RoleScheduleDialogState copyWith({List<RoleScheduleEntity>? schedule}) {
|
||||
return RoleScheduleDialogState(
|
||||
schedule: schedule ?? this.schedule,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/application/common/int_extensions.dart';
|
||||
import 'package:krow/core/entity/role_schedule_entity.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/kw_time_slot.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
|
||||
import 'package:krow/features/create_event/presentation/role_schedule_dialog/bloc/role_schedule_dialog_bloc.dart';
|
||||
|
||||
class RecurringScheduleDialog extends StatefulWidget {
|
||||
const RecurringScheduleDialog({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<RecurringScheduleDialog> createState() =>
|
||||
_RecurringScheduleDialogState();
|
||||
|
||||
static Future<List<RoleScheduleEntity>?> show(
|
||||
BuildContext context, List<RoleScheduleEntity>? schedule) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return BlocProvider(
|
||||
create: (context) => RoleScheduleDialogBloc(schedule ?? []),
|
||||
child: const RecurringScheduleDialog(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RecurringScheduleDialogState extends State<RecurringScheduleDialog> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<RoleScheduleDialogBloc, RoleScheduleDialogState>(
|
||||
builder: (context, state) {
|
||||
return Center(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.symmetric(vertical: 24),
|
||||
decoration: KwBoxDecorations.white24,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildHeader(context),
|
||||
const Gap(24),
|
||||
_buildDayTabs(state.schedule),
|
||||
const Gap(24),
|
||||
...state.schedule.map((e) => _buildTimeSlots(e)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: KwButton.primary(
|
||||
label: 'Save Slots',
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(state.schedule);
|
||||
},
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: KwButton.outlinedPrimary(
|
||||
label: 'Cancel',
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Event Schedule',
|
||||
style: AppTextStyles.bodyLargeMed,
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Assets.images.icons.x.svg(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 24),
|
||||
child: Text(
|
||||
'Select the days and time slots that you want this schedule to recur on',
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_buildDayTabs(List<RoleScheduleEntity> schedule) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(left: 24, right: 20),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
||||
.indexed
|
||||
.map((e) {
|
||||
var (index, item) = e;
|
||||
var selected = schedule.any((element) => element.dayIndex == index);
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
BlocProvider.of<RoleScheduleDialogBloc>(context)
|
||||
.add(TapOnScheduleDayEvent(index));
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
margin: const EdgeInsets.only(right: 4),
|
||||
height: 46,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(23),
|
||||
color: selected ? AppColors.bgColorDark : Colors.transparent,
|
||||
border: Border.all(
|
||||
color: selected ? AppColors.bgColorDark : AppColors.grayStroke,
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
item,
|
||||
style: AppTextStyles.bodyMediumReg.copyWith(
|
||||
color: selected ? AppColors.grayWhite : AppColors.blackGray),
|
||||
)),
|
||||
),
|
||||
);
|
||||
}).toList()),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTimeSlots(RoleScheduleEntity e) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 24, right: 24, bottom: 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: e.dayIndex.getWeekdayId(),
|
||||
style: AppTextStyles.bodyMediumReg.copyWith(height: 1),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: ' Time Slot',
|
||||
style: AppTextStyles.bodyTinyReg
|
||||
.copyWith(color: AppColors.blackGray, height: 1),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
_timeRow(context, e),
|
||||
buildScheduleButton(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_timeRow(context, RoleScheduleEntity entity) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: KwTimeSlotInput(
|
||||
key: ValueKey('dialog_start_time_key_${entity.dayIndex}'),
|
||||
label: 'Start Time',
|
||||
initialValue: entity.startTime,
|
||||
onChange: (value) {
|
||||
BlocProvider.of<RoleScheduleDialogBloc>(context)
|
||||
.add(SetRoleScheduleStartTimeEvent(entity, value));
|
||||
},
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: KwTimeSlotInput(
|
||||
key: ValueKey('dialog_start_time_key_${entity.dayIndex}'),
|
||||
label: 'End Time',
|
||||
initialValue: entity.endTime,
|
||||
onChange: (value) {
|
||||
BlocProvider.of<RoleScheduleDialogBloc>(context)
|
||||
.add(SetRoleScheduleEndTimeEvent(entity, value));
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
GestureDetector buildScheduleButton() {
|
||||
return GestureDetector(
|
||||
onTap: () {},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 12),
|
||||
height: 20,
|
||||
child: Row(
|
||||
children: [
|
||||
Assets.images.icons.copy.svg(),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'Copy Time to All',
|
||||
style: AppTextStyles.bodyMediumMed
|
||||
.copyWith(color: AppColors.primaryBlue),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/application/common/int_extensions.dart';
|
||||
import 'package:krow/core/entity/role_schedule_entity.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/features/create_event/presentation/create_role_section/bloc/create_role_bloc.dart';
|
||||
import 'package:krow/features/create_event/presentation/role_schedule_dialog/recurring_schedule_dialog.dart';
|
||||
|
||||
class RecurringScheduleWidget extends StatelessWidget {
|
||||
const RecurringScheduleWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CreateRoleBloc, CreateRoleState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (state.entity.schedule != null &&
|
||||
(state.entity.schedule?.isNotEmpty ?? false)) ...[
|
||||
const Gap(12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: const Text(
|
||||
'Schedule',
|
||||
style: AppTextStyles.bodyMediumReg,
|
||||
),
|
||||
),
|
||||
...state.entity.schedule!.map((e) => _buildScheduleItem(e)),
|
||||
const Gap(8),
|
||||
],
|
||||
buildScheduleButton(context, state),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
GestureDetector buildScheduleButton(
|
||||
BuildContext context, CreateRoleState state) {
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
var result =
|
||||
await RecurringScheduleDialog.show(context, state.entity.schedule);
|
||||
if (result != null) {
|
||||
context
|
||||
.read<CreateRoleBloc>()
|
||||
.add(CreateRoleSetScheduleEvent(result));
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 12),
|
||||
height: 20,
|
||||
child: Row(
|
||||
children: [
|
||||
Assets.images.icons.calendarEdit.svg(),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'Set Work Days',
|
||||
style: AppTextStyles.bodyMediumMed
|
||||
.copyWith(color: AppColors.primaryBlue),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildScheduleItem(RoleScheduleEntity e) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
height: 46,
|
||||
alignment: Alignment.centerLeft,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(23),
|
||||
border: Border.all(color: AppColors.grayStroke, width: 1),
|
||||
),
|
||||
child: Text(e.dayIndex.getWeekdayId()),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_input.dart';
|
||||
import 'package:krow/features/create_event/domain/bloc/create_event_bloc.dart';
|
||||
|
||||
class AddInfoInputWidget extends StatefulWidget {
|
||||
const AddInfoInputWidget({super.key});
|
||||
|
||||
@override
|
||||
State<AddInfoInputWidget> createState() => _AddInfoInputWidgetState();
|
||||
}
|
||||
|
||||
class _AddInfoInputWidgetState extends State<AddInfoInputWidget> {
|
||||
final TextEditingController _addInfoController = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<CreateEventBloc, CreateEventState>(
|
||||
listener: (context, state) {
|
||||
if (state.entity.additionalInfo != _addInfoController.text) {
|
||||
_addInfoController.text = state.entity.additionalInfo??'';
|
||||
}
|
||||
},
|
||||
buildWhen: (previous, current) {
|
||||
return previous.entity.additionalInfo != current.entity.additionalInfo;
|
||||
},
|
||||
builder: (context, state) {
|
||||
return KwTextInput(
|
||||
controller: _addInfoController,
|
||||
onChanged: (value) {
|
||||
BlocProvider.of<CreateEventBloc>(context)
|
||||
.add(CreateEventAddInfoChange(value));
|
||||
},
|
||||
maxLength: 300,
|
||||
showCounter: true,
|
||||
minHeight: 144,
|
||||
hintText: 'Enter your main text here...',
|
||||
title: 'Additional Information',
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/features/create_event/domain/bloc/create_event_bloc.dart';
|
||||
|
||||
class AddonsSectionWidget extends StatelessWidget {
|
||||
const AddonsSectionWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CreateEventBloc, CreateEventState>(
|
||||
builder: (context, state) {
|
||||
var allAddons = state.addons;
|
||||
var selectedAddons = state.entity.addons;
|
||||
if(context.read<CreateEventBloc>().state.addons.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
const Divider(
|
||||
height: 0,
|
||||
color: AppColors.grayStroke,
|
||||
),
|
||||
for (var addon in allAddons)
|
||||
_addonStrokeItem(
|
||||
title: addon.name ?? '',
|
||||
enabled: selectedAddons
|
||||
?.any((selected) => selected.id == addon.id) ??
|
||||
false,
|
||||
onTap: () {
|
||||
BlocProvider.of<CreateEventBloc>(context)
|
||||
.add(CreateEventToggleAddon(addon));
|
||||
},
|
||||
),
|
||||
const Gap(12),
|
||||
const Divider(
|
||||
height: 0,
|
||||
color: AppColors.grayStroke,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
_addonStrokeItem(
|
||||
{required String title,
|
||||
required bool enabled,
|
||||
required VoidCallback onTap}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
)),
|
||||
CupertinoSwitch(
|
||||
value: enabled,
|
||||
onChanged: (_) {
|
||||
onTap();
|
||||
},
|
||||
activeTrackColor: AppColors.bgColorDark,
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/data/models/event/hub_model.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_dropdown.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_input.dart';
|
||||
import 'package:krow/features/create_event/domain/bloc/create_event_bloc.dart';
|
||||
import 'package:krow/features/create_event/presentation/event_date_section/event_date_input_widget.dart';
|
||||
|
||||
import '../../../../core/entity/event_entity.dart';
|
||||
|
||||
class CreateEventDetailsCardWidget extends StatefulWidget {
|
||||
const CreateEventDetailsCardWidget({super.key});
|
||||
|
||||
@override
|
||||
State<CreateEventDetailsCardWidget> createState() =>
|
||||
_CreateEventDetailsCardWidgetState();
|
||||
}
|
||||
|
||||
class _CreateEventDetailsCardWidgetState
|
||||
extends State<CreateEventDetailsCardWidget>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
TextEditingController poNumberController = TextEditingController();
|
||||
// TextEditingController contractNumberController = TextEditingController();
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return BlocConsumer<CreateEventBloc, CreateEventState>(
|
||||
listener: (context, state) {
|
||||
if (state.entity.name != _nameController.text) {
|
||||
_nameController.text = state.entity.name;
|
||||
}
|
||||
|
||||
// if (state.entity.contractNumber != null &&
|
||||
// state.entity.contractNumber != contractNumberController.text) {
|
||||
// contractNumberController.text = state.entity.contractNumber!;
|
||||
// }
|
||||
//
|
||||
if (state.entity.poNumber != null &&
|
||||
state.entity.poNumber != poNumberController.text) {
|
||||
poNumberController.text = state.entity.poNumber!;
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return AnimatedContainer(
|
||||
duration: Duration(milliseconds: 300),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 24),
|
||||
decoration: KwBoxDecorations.white12.copyWith(
|
||||
border: state.validationState != null
|
||||
? Border.all(color: AppColors.statusError, width: 1)
|
||||
: null),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Event Details',
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
),
|
||||
const Gap(12),
|
||||
KwTextInput(
|
||||
controller: _nameController,
|
||||
title: 'Event Name',
|
||||
hintText: 'Enter event name',
|
||||
onChanged: (value) {
|
||||
BlocProvider.of<CreateEventBloc>(context)
|
||||
.add(CreateEventNameChange(value));
|
||||
},
|
||||
),
|
||||
IgnorePointer(
|
||||
ignoring: ![EventStatus.draft, EventStatus.pending].contains(state.entity.status) && state.entity.status!=null,
|
||||
child: EventDateInputWidget(state.entity)),
|
||||
const Gap(8),
|
||||
_hubDropdown(state.hubs, state),
|
||||
const Gap(8),
|
||||
// _contractDropdown(context, state),
|
||||
// if (state.entity.contractType == EventContractType.contract)
|
||||
// _buildContractInput(),
|
||||
// if (state.entity.contractType == EventContractType.purchaseOrder)
|
||||
_buildPurchaseInput(),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Column _hubDropdown(List<HubModel> hubs, CreateEventState state) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
child: Text(
|
||||
'Location',
|
||||
style:
|
||||
AppTextStyles.bodyTinyReg.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
KwDropdown(
|
||||
horizontalPadding: 28,
|
||||
hintText: 'Hub name',
|
||||
selectedItem: state.entity.hub != null
|
||||
? KwDropDownItem(
|
||||
data: state.entity.hub, title: state.entity.hub?.name ?? '')
|
||||
: null,
|
||||
items: hubs
|
||||
.map((e) => KwDropDownItem(data: e, title: e.name ?? ''))
|
||||
.toList(),
|
||||
onSelected: (item) {
|
||||
BlocProvider.of<CreateEventBloc>(context)
|
||||
.add(CreateEventChangeHub(item!));
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Column _contractDropdown(BuildContext context, CreateEventState state) {
|
||||
// return Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// children: [
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.only(left: 16),
|
||||
// child: Text(
|
||||
// 'Payment type',
|
||||
// style:
|
||||
// AppTextStyles.bodyTinyReg.copyWith(color: AppColors.blackGray),
|
||||
// ),
|
||||
// ),
|
||||
// const Gap(4),
|
||||
// KwDropdown(
|
||||
// horizontalPadding: 28,
|
||||
// hintText: 'Direct',
|
||||
// selectedItem: KwDropDownItem(
|
||||
// data: state.entity.contractType,
|
||||
// title: state.entity.contractType.formattedName),
|
||||
// items: const [
|
||||
// KwDropDownItem(data: EventContractType.direct, title: 'Direct'),
|
||||
// KwDropDownItem(
|
||||
// data: EventContractType.purchaseOrder,
|
||||
// title: 'Purchase Order'),
|
||||
// KwDropDownItem(
|
||||
// data: EventContractType.contract, title: 'Contract'),
|
||||
// ],
|
||||
// onSelected: (item) {
|
||||
// BlocProvider.of<CreateEventBloc>(context)
|
||||
// .add(CreateEventChangeContractType(item));
|
||||
// }),
|
||||
// ],
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// _buildContractInput() {
|
||||
// return Padding(
|
||||
// padding: const EdgeInsets.only(top:8.0),
|
||||
// child: KwTextInput(
|
||||
// controller: contractNumberController,
|
||||
// onChanged: (value) {
|
||||
// BlocProvider.of<CreateEventBloc>(context)
|
||||
// .add(CreateEventChangeContractNumber(value));
|
||||
// },
|
||||
// title: 'Contract number',
|
||||
// hintText: '#00000'),
|
||||
// );
|
||||
// }
|
||||
//
|
||||
_buildPurchaseInput() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top:8.0),
|
||||
child: KwTextInput(
|
||||
controller: poNumberController,
|
||||
onChanged: (value) {
|
||||
BlocProvider.of<CreateEventBloc>(context)
|
||||
.add(CreateEventChangePoNumber(value));
|
||||
},
|
||||
title: 'PO Reference number',
|
||||
hintText: 'PO Reference number'),
|
||||
);
|
||||
}
|
||||
}
|
||||
//
|
||||
// extension on EventContractType? {
|
||||
// String get formattedName {
|
||||
// return switch (this) {
|
||||
// EventContractType.direct => 'Direct',
|
||||
// EventContractType.contract => 'Contract',
|
||||
// EventContractType.purchaseOrder => 'Purchase Order',
|
||||
// null => 'null'
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,136 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/data/models/event/tag_model.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/features/create_event/domain/bloc/create_event_bloc.dart';
|
||||
|
||||
class CreateEventTagsCard extends StatelessWidget {
|
||||
const CreateEventTagsCard({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CreateEventBloc, CreateEventState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.tags != current.tags ||
|
||||
previous.entity.tags != current.entity.tags,
|
||||
builder: (context, state) {
|
||||
if(state.tags.isEmpty) return const SizedBox();
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.only(top: 12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 24),
|
||||
decoration: KwBoxDecorations.white12,
|
||||
child: _buildChips(context, state.tags, state.entity.tags ?? []),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChips(BuildContext context, List<TagModel> allTags,
|
||||
List<TagModel> selectedTags) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 12),
|
||||
child: Wrap(
|
||||
runSpacing: 8,
|
||||
spacing: 8,
|
||||
children: allTags
|
||||
.map((e) =>
|
||||
_buildTag(context, e, selectedTags.any((t) => e.id == t.id)))
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTag(BuildContext context, TagModel tag, bool selected) {
|
||||
const duration = Duration(milliseconds: 150);
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
BlocProvider.of<CreateEventBloc>(context)
|
||||
.add(CreateEventTagSelected(tag));
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: duration,
|
||||
padding: const EdgeInsets.all(8),
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: selected ? AppColors.blackBlack : AppColors.grayTintStroke,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
duration: duration,
|
||||
height: 28,
|
||||
width: 28,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: selected ? AppColors.blackBlack : AppColors.tintGray,
|
||||
),
|
||||
child: Center(
|
||||
child: getImageById(tag.id).svg(
|
||||
width: 12.0,
|
||||
height: 12.0,
|
||||
colorFilter: ColorFilter.mode(
|
||||
selected ? AppColors.grayWhite : AppColors.blackBlack,
|
||||
BlendMode.srcIn)),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
tag.name,
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.bgColorDark),
|
||||
),
|
||||
const Gap(8),
|
||||
AnimatedContainer(
|
||||
duration: duration,
|
||||
height: 16,
|
||||
width: 16,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: selected
|
||||
? AppColors.blackBlack
|
||||
: AppColors.grayTintStroke,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: AnimatedContainer(
|
||||
duration: duration,
|
||||
height: 6,
|
||||
width: 6,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color:
|
||||
selected ? AppColors.blackBlack : Colors.transparent,
|
||||
)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
getImageById(String id) {
|
||||
switch (id) {
|
||||
case '1':
|
||||
return Assets.images.icons.tags.award;
|
||||
case '2':
|
||||
return Assets.images.icons.tags.briefcase;
|
||||
case '3':
|
||||
return Assets.images.icons.tags.flash;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
|
||||
class CreateEventTitleWidget extends StatelessWidget {
|
||||
const CreateEventTitleWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Gap(8),
|
||||
const Text('Create Your Event', style: AppTextStyles.headingH1),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'Bring your vision to life! Share details, set the stage, and connect with your audience—your event starts here.',
|
||||
style:
|
||||
AppTextStyles.bodyMediumReg.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
const Gap(24),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/features/create_event/domain/bloc/create_event_bloc.dart';
|
||||
|
||||
class TotalCostRowWidget extends StatelessWidget {
|
||||
TotalCostRowWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CreateEventBloc, CreateEventState>(
|
||||
builder: (context, state) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Approximate Total Costs',
|
||||
style: AppTextStyles.bodyMediumReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: state.entity.totalCost,
|
||||
builder: (context, value, child) {
|
||||
return Text(
|
||||
'\$${value.toStringAsFixed(2)}',
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user