feat: Refactor code structure and optimize performance across multiple modules

This commit is contained in:
Achintha Isuru
2025-11-17 23:29:28 -05:00
parent 831570f2e0
commit a64cbd9edf
1508 changed files with 105319 additions and 0 deletions

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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
}
}
''';

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}

View File

@@ -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,
);
}
}

View File

@@ -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);
}

View File

@@ -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'],
);
}
}

View File

@@ -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;
}
}

View File

@@ -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,
);
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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),
],
);
}
}

View File

@@ -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),
);
},
);
},
);
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}

View File

@@ -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
);
}
}

View File

@@ -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));
},
);
}
}

View File

@@ -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),
);
},
);
},
);
}
}

View File

@@ -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();
});
}
}

View File

@@ -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),
),
),
),
),
],
),
);
}
}

View File

@@ -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,
));
}
}

View File

@@ -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);
}

View File

@@ -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,
);
}
}

View File

@@ -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,
);
}
}

View File

@@ -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,
),
);
}
}

View File

@@ -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',
])
];
}
}

View File

@@ -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()));
}
}

View File

@@ -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();
}

View File

@@ -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,
);
}
}

View File

@@ -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),
),
],
),
),
);
}
}

View File

@@ -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()),
);
}
}

View File

@@ -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',
);
},
);
}
}

View File

@@ -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,
)
],
),
);
}
}

View File

@@ -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'
// };
// }
// }

View File

@@ -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;
}
}
}

View File

@@ -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),
],
);
}
}

View File

@@ -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,
);
},
)
],
);
},
);
}
}