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,50 @@
const String getStaffScheduleSchema = '''
query GetStaffSchedule {
me {
id
schedule {
id
type
start_at
end_at
day_of_week
}
}
}
''';
const String createStaffScheduleMutationSchema = r'''
mutation CreateStaffSchedule($input: [StaffScheduleInput!]!) {
create_staff_schedule(input: $input) {
id
type
start_at
end_at
day_of_week
}
}
''';
const String updateStaffScheduleMutationSchema = r'''
mutation UpdateStaffSchedule($id: ID!, $input: StaffScheduleInput!) {
update_staff_schedule(id: $id, input: $input) {
id
type
start_at
end_at
day_of_week
}
}
''';
const String deleteStaffScheduleMutationSchema = r'''
mutation DeleteStaffSchedule($ids: [ID!]!) {
delete_staff_schedule(ids: $ids) {
id
type
start_at
end_at
day_of_week
}
}
''';

View File

@@ -0,0 +1,13 @@
import 'package:krow/features/profile/schedule/data/models/schedule_slot_model.dart';
class DayScheduleModel {
DayScheduleModel({
required this.date,
required this.slots,
this.isWeekly = false,
});
final DateTime date;
final bool isWeekly;
List<ScheduleSlotModel> slots;
}

View File

@@ -0,0 +1,38 @@
import 'package:krow/core/application/common/date_time_extension.dart';
class ScheduleSlotModel {
ScheduleSlotModel({
required this.isWeekly,
required this.startAt,
required this.endAt,
this.id,
});
factory ScheduleSlotModel.fromJson(Map<String, dynamic> data) {
return ScheduleSlotModel(
id: data['id'] as String?,
isWeekly: data['type'] == 'weekly',
startAt: DateTime.parse(data['start_at'] as String),
endAt: DateTime.parse(data['end_at'] as String),
);
}
final String? id;
final bool isWeekly;
final DateTime startAt;
final DateTime endAt;
// TODO(Sleep): For now has to use .replaceAll('.000', '') for trim DateTime otherwise will result in error from backend.
Map<String, dynamic> toJson() {
return {
// if (id != null) 'id': id,
'type': isWeekly ? 'weekly' : 'range',
'start_at': startAt.toString().replaceAll('.000', ''),
'end_at': endAt.toString().replaceAll('.000', ''),
'day_of_week': startAt.getWeekdayId(),
};
}
String getIdKey() =>
isWeekly ? startAt.getWeekdayId() : startAt.getDayDateId();
}

View File

@@ -0,0 +1,180 @@
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:injectable/injectable.dart';
import 'package:krow/core/application/clients/api/api_client.dart';
import 'package:krow/features/profile/schedule/data/gql_schemas.dart';
import 'package:krow/features/profile/schedule/data/models/day_schedule_model.dart';
import 'package:krow/features/profile/schedule/data/models/schedule_slot_model.dart';
@injectable
class StaffScheduleApiProvider {
final ApiClient _apiClient;
StaffScheduleApiProvider(this._apiClient);
Map<String, DayScheduleModel> _processScheduleDataList(
List<dynamic> scheduleData,
) {
final schedules = [
for (final scheduleItem in scheduleData)
ScheduleSlotModel.fromJson(
scheduleItem as Map<String, dynamic>,
),
];
Map<String, DayScheduleModel> scheduleMap = {};
for (int i = 0; i < schedules.length; i++) {
final scheduleKey = schedules[i].getIdKey();
var scheduleDay = scheduleMap[scheduleKey];
if (scheduleDay == null) {
scheduleDay = DayScheduleModel(
date: schedules[i].startAt.copyWith(
hour: 0,
minute: 0,
second: 0,
),
slots: [schedules[i]],
isWeekly: schedules[i].isWeekly,
);
scheduleMap[scheduleKey] = scheduleDay;
} else {
scheduleDay.slots.add(schedules[i]);
}
}
return scheduleMap;
}
Future<Map<String, DayScheduleModel>> _handleResultReturn(
QueryResult<Object?> result,
String mutationKey,
) async {
if (result.hasException) throw Exception(result.exception.toString());
if (result.data == null || result.data!.isEmpty) return {};
final scheduleData = result.data?[mutationKey] as List<dynamic>? ?? [];
return _processScheduleDataList(scheduleData);
}
Future<Map<String, DayScheduleModel>> getStaffSchedule() async {
final response = await _apiClient.query(schema: getStaffScheduleSchema);
if (response.hasException) {
throw Exception(response.exception.toString());
}
final scheduleData = (response.data?['me']
as Map<String, dynamic>?)?['schedule'] as List<dynamic>? ??
[];
return _processScheduleDataList(scheduleData);
}
Stream<Map<String, DayScheduleModel>> getStaffScheduleWithCache() async* {
await for (var response in _apiClient.queryWithCache(
schema: getStaffScheduleSchema,
)) {
if (response == null || response.data == null) continue;
if (response.data == null || response.data!.isEmpty) {
if (response.source?.name == 'cache') continue;
if (response.hasException) {
throw Exception(response.exception.toString());
}
}
final scheduleData = (response.data?['me']
as Map<String, dynamic>?)?['schedule'] as List<dynamic>? ??
[];
yield _processScheduleDataList(scheduleData);
}
}
Future<Map<String, DayScheduleModel>> createStaffSchedule(
List<ScheduleSlotModel> schedules,
) async {
final result = await _apiClient.mutate(
schema: createStaffScheduleMutationSchema,
body: {
'input': [
for (final schedule in schedules) schedule.toJson(),
],
},
);
return _handleResultReturn(result, 'create_staff_schedule');
}
Future<Map<String, DayScheduleModel>> updateStaffSchedule(
List<ScheduleSlotModel> schedules,
) async {
// var result = await _apiClient.mutate(
// schema: updateStaffScheduleMutationSchema,
// body: {
// 'input': [
// for (final schedule in schedules) schedule.toJson(),
// ],
// },
// );
await Future.wait(
[
for (final schedule in schedules)
_apiClient.mutate(
schema: updateStaffScheduleMutationSchema,
body: {
'id': schedule.id,
'input': schedule.toJson(),
},
),
],
);
final result = await _apiClient.mutate(
schema: updateStaffScheduleMutationSchema,
body: {
'id': schedules.last.id,
'input': schedules.last.toJson(),
},
);
return _handleResultReturn(result, 'update_staff_schedule');
}
Future<ScheduleSlotModel?> deleteStaffSchedule(
List<ScheduleSlotModel> schedules,
) async {
final result = await _apiClient.mutate(
schema: deleteStaffScheduleMutationSchema,
body: {
'ids': [
for (final schedule in schedules) schedule.id,
],
},
);
if (result.hasException) throw Exception(result.exception.toString());
if (result.data == null || result.data!.isEmpty) return null;
//TODO: For now backend returns only one value so there is no
// point in returning it
return null;
}
Future<ScheduleSlotModel?> deleteStaffScheduleById(List<String> ids) async {
final result = await _apiClient.mutate(
schema: deleteStaffScheduleMutationSchema,
body: {'ids': ids},
);
if (result.hasException) throw Exception(result.exception.toString());
return null;
}
}

View File

@@ -0,0 +1,116 @@
import 'package:injectable/injectable.dart';
import 'package:krow/features/profile/schedule/data/models/day_schedule_model.dart';
import 'package:krow/features/profile/schedule/data/models/schedule_slot_model.dart';
import 'package:krow/features/profile/schedule/data/staff_schedule_api_provider.dart';
import 'package:krow/features/profile/schedule/domain/entities/day_shedule.dart';
import 'package:krow/features/profile/schedule/domain/entities/schedule_slot.dart';
import 'package:krow/features/profile/schedule/domain/staff_schedule_repository.dart';
@Injectable(as: StaffScheduleRepository)
class StaffScheduleRepositoryImpl extends StaffScheduleRepository {
StaffScheduleRepositoryImpl({
required StaffScheduleApiProvider apiProvider,
}) : _apiProvider = apiProvider;
final StaffScheduleApiProvider _apiProvider;
Map<String, DaySchedule> _processSchedulesMap(
Map<String, DayScheduleModel> daySchedulesData,
) {
return daySchedulesData.map(
(key, dayEntry) {
return MapEntry(
key,
DaySchedule(
date: dayEntry.date,
isWeekly: dayEntry.isWeekly,
slots: [
for (final slot in dayEntry.slots)
ScheduleSlot(
id: slot.startAt.microsecondsSinceEpoch.toString(),
startTime: slot.startAt,
endTime: slot.endAt,
remoteId: slot.id,
),
],
),
);
},
);
}
List<ScheduleSlotModel> _expandDailySchedulesToSlots(
List<DaySchedule> schedules,
) {
return [
for (final daySchedule in schedules) ...[
for (final slot in daySchedule.slots)
ScheduleSlotModel(
isWeekly: daySchedule.isWeekly,
startAt: slot.startTime,
endAt: slot.endTime,
id: slot.remoteId,
),
],
];
}
@override
Stream<Map<String, DaySchedule>> getStaffSchedule() async* {
await for (final daySchedulesData
in _apiProvider.getStaffScheduleWithCache()) {
yield _processSchedulesMap(daySchedulesData);
}
}
@override
Future<Map<String, DaySchedule>> createStaffSchedule({
required List<DaySchedule> schedules,
}) async {
return _processSchedulesMap(
await _apiProvider.createStaffSchedule(
_expandDailySchedulesToSlots(schedules),
),
);
}
@override
Future<Map<String, DaySchedule>> updateStaffSchedule({
required List<DaySchedule> schedules,
}) async {
if (schedules.length == 1) {
if (schedules.first.deletedSlots != null) {
await _apiProvider.deleteStaffScheduleById(
[
for (final deletedSlot in schedules.first.deletedSlots!)
deletedSlot.remoteId ?? '',
],
);
}
// In case there were only deletion of slots for the day, and no edits
if (schedules.first.slots.isEmpty) {
return _processSchedulesMap(
await _apiProvider.getStaffSchedule(),
);
}
}
return _processSchedulesMap(
await _apiProvider.updateStaffSchedule(
_expandDailySchedulesToSlots(schedules),
),
);
}
@override
Future<DaySchedule?> deleteStaffSchedule({
required List<DaySchedule> schedules,
}) async {
await _apiProvider.deleteStaffSchedule(
_expandDailySchedulesToSlots(schedules),
);
return null;
}
}

View File

@@ -0,0 +1,337 @@
import 'dart:developer';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow/core/application/common/date_time_extension.dart';
import 'package:krow/core/application/di/injectable.dart';
import 'package:krow/core/data/enums/state_status.dart';
import 'package:krow/features/profile/schedule/domain/entities/day_shedule.dart';
import 'package:krow/features/profile/schedule/domain/entities/schedule_slot.dart';
import 'package:krow/features/profile/schedule/domain/staff_schedule_repository.dart';
part 'schedule_event.dart';
part 'schedule_state.dart';
class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
ScheduleBloc() : super(const ScheduleState()) {
on<ScheduleInitEvent>(_onBlocInit);
on<ScheduleEventChangeMode>(_onChangeMode);
on<ScheduleEventSelectDates>(_onSelectDates);
on<ScheduleEventAddSlot>(_onAddSlot);
on<ScheduleEventRemoveSlot>(_onRemoveSlot);
on<ScheduleEventEditSlot>(_onEditSlot);
on<ScheduleEventSave>(_onConfirmChanges);
on<ScheduleEventCancel>(_onCancelChanges);
on<ScheduleEventDeleteSchedule>(_onDeleteSchedule);
}
final StaffScheduleRepository staffRepository =
getIt<StaffScheduleRepository>();
Future<void> _onBlocInit(
ScheduleInitEvent event,
Emitter<ScheduleState> emit,
) async {
emit(state.copyWith(status: StateStatus.loading));
try {
await for (final scheduleData in staffRepository.getStaffSchedule()) {
emit(
state.copyWith(
schedules: scheduleData,
status: StateStatus.idle,
),
);
}
} catch (except) {
log(except.toString());
}
if (state.status == StateStatus.loading) {
emit(state.copyWith(status: StateStatus.idle));
}
}
Future<void> _onSelectDates(
ScheduleEventSelectDates event,
Emitter<ScheduleState> emit,
) async {
if (event.dates.length == 1) {
emit(
state.copyWith(
scheduleErrorText: '',
selectedDates: [event.dates.first],
prevSelectedDates: [],
isInEditMode: false,
),
);
emit(
state.copyWith(
selectedSchedules: await state.findSchedulesForSelection(),
),
);
return;
}
var startDate = event.dates.first;
final endDate = event.dates.last;
emit(
state.copyWith(
scheduleErrorText: '',
selectedDates: [
for (var i = 0; i < endDate.difference(startDate).inDays; i++)
startDate.add(Duration(days: i)),
endDate,
],
prevSelectedDates: [],
isInEditMode: false,
),
);
emit(
state.copyWith(
selectedSchedules: await state.findSchedulesForSelection(),
),
);
}
void _onChangeMode(
ScheduleEventChangeMode event,
Emitter<ScheduleState> emit,
) {
if (event.isInScheduleAddMode != null && state.selectedDates.length == 1) {
emit(state.copyWith(
scheduleErrorText: '',
isScheduleAddMode: event.isInScheduleAddMode,
));
return;
}
if (event.editedDate != null) {
emit(
state.copyWith(
selectedDates: [event.editedDate!],
prevSelectedDates: List.from(state.selectedDates),
),
);
}
emit(
state.copyWith(
isInEditMode: event.isInEditScheduleMode,
scheduleErrorText: '',
isScheduleAddMode: false,
tempSchedules: state.selectedDates.length == 1
? [
state.getScheduleForDate(
date: state.selectedDates.first,
isWeekly: event.isWeekly,
),
]
: [
for (int i = 0; i < state.selectedDates.length; i++)
DaySchedule(
date: state.selectedDates[i],
slots: const [],
),
],
),
);
}
void _onAddSlot(
ScheduleEventAddSlot event,
Emitter<ScheduleState> emit,
) {
emit(
state.copyWith(
tempSchedules: state.tempSchedules.map(
(daySchedule) {
final startTime = daySchedule.getLatestSlot();
return daySchedule.copyWith(
slots: [
...daySchedule.slots,
ScheduleSlot.minFromStartTime(
start: startTime,
),
],
);
},
).toList(),
),
);
}
void _onRemoveSlot(
ScheduleEventRemoveSlot event,
Emitter<ScheduleState> emit,
) {
emit(
state.copyWith(
scheduleErrorText: '',
tempSchedules: state.tempSchedules.map(
(daySchedule) {
return daySchedule.removeSlotAtIndex(event.slotIndex);
},
).toList(),
),
);
}
void _onEditSlot(
ScheduleEventEditSlot event,
Emitter<ScheduleState> emit,
) {
emit(
state.copyWith(
scheduleErrorText: '',
tempSchedules: state.tempSchedules.map(
(daySchedule) {
final daySlots = List<ScheduleSlot>.from(daySchedule.slots);
daySlots[event.slotIndex] = daySlots[event.slotIndex].editTime(
startTime: event.start?.copyWith(
day: daySchedule.date.day,
month: daySchedule.date.month,
),
endTime: event.end?.copyWith(
day: daySchedule.date.day,
month: daySchedule.date.month,
),
);
return daySchedule.copyWith(
slots: daySlots,
);
},
).toList(),
),
);
}
Future<Map<String, DaySchedule>> _handleTemporaryScheduleSave() async {
final previousSchedules = state.getDailySchedulesForTemporary();
if (state.tempSchedules.length == 1) {
return previousSchedules.isNotEmpty
? staffRepository.updateStaffSchedule(
schedules: state.tempSchedules,
)
: staffRepository.createStaffSchedule(
schedules: state.tempSchedules,
);
}
if (previousSchedules.isNotEmpty) {
await staffRepository.deleteStaffSchedule(schedules: previousSchedules);
}
return staffRepository.createStaffSchedule(
schedules: state.tempSchedules,
);
}
Future<void> _onConfirmChanges(
ScheduleEventSave event,
Emitter<ScheduleState> emit,
) async {
emit(
state.copyWith(
status: StateStatus.loading,
selectedDates:
state.prevSelectedDates.isNotEmpty ? state.prevSelectedDates : null,
prevSelectedDates: [],
),
);
final isTempScheduleValid = await state.isValid;
if (!isTempScheduleValid) {
emit(
state.copyWith(
status: StateStatus.error,
scheduleErrorText: 'overlap_error'.tr(),
),
);
return;
}
Map<String, DaySchedule>? schedules;
try {
schedules = await _handleTemporaryScheduleSave();
} catch (except) {
log(except.toString());
}
emit(
state.copyWith(
status: StateStatus.idle,
isInEditMode: false,
schedules: schedules,
tempSchedules: [],
),
);
emit(
state.copyWith(
selectedSchedules: await state.findSchedulesForSelection(),
),
);
}
void _onCancelChanges(
ScheduleEventCancel event,
Emitter<ScheduleState> emit,
) {
emit(
state.copyWith(
isInEditMode: false,
scheduleErrorText: '',
tempSchedules: [],
selectedDates:
state.prevSelectedDates.isNotEmpty ? state.prevSelectedDates : null,
prevSelectedDates: [],
),
);
}
Future<void> _onDeleteSchedule(
ScheduleEventDeleteSchedule event,
Emitter<ScheduleState> emit,
) async {
emit(
state.copyWith(
isInEditMode: false,
status: StateStatus.loading,
),
);
Map<String, DaySchedule>? schedules;
try {
final result = staffRepository.deleteStaffSchedule(
schedules: [event.schedule],
);
schedules = Map<String, DaySchedule>.from(state.schedules);
await result;
schedules.remove(event.schedule.getIdKey());
} catch (except) {
log(except.toString());
}
emit(
state.copyWith(
status: StateStatus.idle,
schedules: schedules,
),
);
emit(
state.copyWith(
selectedSchedules: await state.findSchedulesForSelection(),
),
);
}
}

View File

@@ -0,0 +1,74 @@
part of 'schedule_bloc.dart';
@immutable
sealed class ScheduleEvent {
const ScheduleEvent();
}
class ScheduleInitEvent extends ScheduleEvent {
const ScheduleInitEvent();
}
class ScheduleEventChangeMode extends ScheduleEvent {
const ScheduleEventChangeMode({
this.isInEditScheduleMode,
this.isInScheduleAddMode,
this.editedDate,
this.isWeekly = false,
});
final bool? isInEditScheduleMode;
final bool? isInScheduleAddMode;
final DateTime? editedDate;
final bool isWeekly;
}
class ScheduleEventSelectDates extends ScheduleEvent {
const ScheduleEventSelectDates({required this.dates});
final List<DateTime> dates;
}
class ScheduleEventAddSlot extends ScheduleEvent {
const ScheduleEventAddSlot();
}
class ScheduleEventRemoveSlot extends ScheduleEvent {
const ScheduleEventRemoveSlot({
required this.slot,
required this.slotIndex,
});
final ScheduleSlot slot;
final int slotIndex;
}
class ScheduleEventDeleteSchedule extends ScheduleEvent {
const ScheduleEventDeleteSchedule({
required this.schedule,
});
final DaySchedule schedule;
}
class ScheduleEventEditSlot extends ScheduleEvent {
const ScheduleEventEditSlot({
required this.slot,
required this.slotIndex,
this.start,
this.end,
});
final ScheduleSlot slot;
final int slotIndex;
final DateTime? start;
final DateTime? end;
}
class ScheduleEventSave extends ScheduleEvent {
const ScheduleEventSave();
}
class ScheduleEventCancel extends ScheduleEvent {
const ScheduleEventCancel();
}

View File

@@ -0,0 +1,122 @@
part of 'schedule_bloc.dart';
@immutable
class ScheduleState {
const ScheduleState({
this.schedules = const {},
this.tempSchedules = const [],
this.selectedDates = const [],
this.prevSelectedDates = const [],
this.selectedSchedules = const [],
this.status = StateStatus.idle,
this.isInEditMode = false,
this.isScheduleAddMode = false,
this.scheduleErrorText = '',
});
final Map<String, DaySchedule> schedules;
final List<DaySchedule> tempSchedules;
final List<DateTime> selectedDates;
final List<DateTime> prevSelectedDates;
final List<DaySchedule> selectedSchedules;
final StateStatus status;
final bool isInEditMode;
final bool isScheduleAddMode;
final String scheduleErrorText;
Future<bool> get isValid async {
if (tempSchedules.isEmpty) return false;
bool isValid = true;
for (int i = 0; i < tempSchedules.length; i++) {
isValid = tempSchedules[i].isValid;
if (!isValid) return isValid;
}
return isValid;
}
ScheduleState copyWith({
Map<String, DaySchedule>? schedules,
List<DaySchedule>? tempSchedules,
List<DateTime>? selectedDates,
List<DateTime>? prevSelectedDates,
List<DaySchedule>? selectedSchedules,
StateStatus? status,
bool? isInEditMode,
bool? isScheduleAddMode,
String? scheduleErrorText,
}) {
return ScheduleState(
schedules: schedules ?? this.schedules,
tempSchedules: tempSchedules ?? this.tempSchedules,
selectedDates: selectedDates ?? this.selectedDates,
prevSelectedDates: prevSelectedDates ?? this.prevSelectedDates,
selectedSchedules: selectedSchedules ?? this.selectedSchedules,
status: status ?? this.status,
isInEditMode: isInEditMode ?? this.isInEditMode,
isScheduleAddMode: isScheduleAddMode ?? this.isScheduleAddMode,
scheduleErrorText: scheduleErrorText ?? this.scheduleErrorText,
);
}
DaySchedule? findScheduleForDate(DateTime date) {
return schedules[date.getDayDateId()] ?? schedules[date.getWeekdayId()];
}
bool containsScheduleForDate(DateTime date) {
return schedules.containsKey(date.getDayDateId()) ||
schedules.containsKey(date.getWeekdayId());
}
Future<List<DaySchedule>> findSchedulesForSelection() async {
return [
for (final date in selectedDates)
if (schedules.containsKey(date.getDayDateId()))
schedules[date.getDayDateId()]!
else if (schedules.containsKey(date.getWeekdayId()))
schedules[date.getWeekdayId()]!.copyWith(
date: date,
),
];
}
List<DaySchedule> getDailySchedulesForTemporary() {
return [
for (final day in tempSchedules)
if (schedules.containsKey(day.getIdKey())) schedules[day.getIdKey()]!
];
}
bool isRangeContainsSchedules(List<DateTime> dates) {
return dates.any(
(date) => schedules.containsKey(date.getDayDateId()),
);
}
DaySchedule getScheduleForDate({
required DateTime date,
required bool isWeekly,
}) {
final daySchedule =
schedules[isWeekly ? date.getWeekdayId() : date.getDayDateId()];
return daySchedule ??
DaySchedule(
date: date,
slots: const [],
isWeekly: isWeekly,
);
}
Map<String, DaySchedule> mergeSchedules() {
final schedules = Map<String, DaySchedule>.from(this.schedules);
for (final schedule in tempSchedules) {
schedules[schedule.getIdKey()] = schedule;
}
return schedules;
}
}

View File

@@ -0,0 +1,75 @@
import 'package:flutter/foundation.dart' show immutable;
import 'package:krow/core/application/common/date_time_extension.dart';
import 'package:krow/features/profile/schedule/domain/entities/schedule_slot.dart';
@immutable
class DaySchedule {
const DaySchedule({
required this.date,
required this.slots,
this.isWeekly = false,
this.deletedSlots,
});
final DateTime date;
final List<ScheduleSlot> slots;
final bool isWeekly;
final List<ScheduleSlot>? deletedSlots;
bool get isValid {
bool isValid = true;
for (int i = 0; i < slots.length; i++) {
for (int k = i; k < slots.length; k++) {
if (k == i) continue; // Skip validation for same slot
isValid = slots[i].isNotOverlappingWith(slots[k]);
if (!isValid) return isValid;
}
}
return isValid;
}
bool get isViableForSaving {
return slots.isNotEmpty || deletedSlots != null;
}
DaySchedule copyWith({
DateTime? date,
List<ScheduleSlot>? slots,
bool? isWeekly,
List<ScheduleSlot>? deletedSlots,
}) {
return DaySchedule(
date: date ?? this.date,
slots: slots ?? this.slots,
isWeekly: isWeekly ?? this.isWeekly,
deletedSlots: deletedSlots ?? this.deletedSlots,
);
}
String getIdKey() => isWeekly ? date.getWeekdayId() : date.getDayDateId();
DateTime getLatestSlot() {
if (slots.isEmpty) return date.copyWith(hour: 8, minute: 0);
var lastSlot = slots.first;
for (int i = 0; i < slots.length; i++) {
if (lastSlot.endTime.isBefore(slots[i].endTime)) {
lastSlot = slots[i];
}
}
return lastSlot.endTime;
}
DaySchedule removeSlotAtIndex(int slotIndex) {
return copyWith(
deletedSlots: slots[slotIndex].remoteId != null
? [...?deletedSlots, slots[slotIndex]]
: null,
slots: List.from(slots)..removeAt(slotIndex),
);
}
}

View File

@@ -0,0 +1,120 @@
import 'package:flutter/foundation.dart' show immutable;
@immutable
class ScheduleSlot {
const ScheduleSlot({
required this.id,
required this.startTime,
required this.endTime,
this.remoteId,
});
factory ScheduleSlot.minFromStartTime({required DateTime start}) {
if (start.hour > 19) start = start.copyWith(hour: 8);
return ScheduleSlot(
id: start.millisecondsSinceEpoch.toString(),
startTime: start.copyWith(minute: 0, second: 0),
endTime: start.copyWith(minute: 0, second: 0).add(
const Duration(hours: 5),
),
);
}
final String id;
final DateTime startTime;
final DateTime endTime;
final String? remoteId;
ScheduleSlot copyWith({
String? id,
DateTime? startTime,
DateTime? endTime,
String? remoteId,
}) {
return ScheduleSlot(
id: id ?? this.id,
startTime: startTime ?? this.startTime,
endTime: endTime ?? this.endTime,
remoteId: remoteId ?? this.remoteId,
);
}
ScheduleSlot editTime({
DateTime? startTime,
DateTime? endTime,
}) {
ScheduleSlot? slot;
if (endTime != null) {
slot = copyWith(
startTime: endTime.difference(this.startTime).inHours.abs() < 5 ||
endTime.isBefore(this.startTime)
? endTime.subtract(const Duration(hours: 5))
: this.startTime,
endTime: endTime,
);
if (slot.startTime.day != slot.endTime.day) {
final DateTime midnight = DateTime(
endTime.year,
endTime.month,
endTime.day,
0,
0,
);
slot = copyWith(
startTime: midnight,
endTime: midnight.add(const Duration(hours: 5)),
);
}
return slot;
} else if (startTime == null) {
slot = this;
} else {
slot = copyWith(
startTime: startTime,
endTime: startTime.difference(this.endTime).inHours.abs() < 5 ||
startTime.isAfter(this.endTime)
? startTime.add(const Duration(hours: 5))
: this.endTime,
);
if (slot.startTime.day != slot.endTime.day) {
final DateTime midnight = DateTime(
startTime.year,
startTime.month,
startTime.day + 1,
0,
0,
);
slot = copyWith(
startTime: midnight.subtract(const Duration(hours: 5)),
endTime: midnight,
);
}
}
return slot;
}
bool isNotOverlappingWith(ScheduleSlot other) {
// End and start time should be before other slot's start time
if (((endTime.isBefore(other.startTime) ||
endTime.isAtSameMomentAs(other.startTime)) &&
startTime.isBefore(other.startTime))) {
return true;
}
// Or start and end time should be after other slot's end time
return ((startTime.isAfter(other.endTime) ||
startTime.isAtSameMomentAs(other.endTime)) &&
endTime.isAfter(other.endTime));
}
@override
String toString() {
return 'Slot data: ID $id\n'
'Start time: $startTime\n'
'End time: $endTime';
}
}

View File

@@ -0,0 +1,17 @@
import 'package:krow/features/profile/schedule/domain/entities/day_shedule.dart';
abstract class StaffScheduleRepository {
Stream<Map<String, DaySchedule>> getStaffSchedule();
Future<Map<String, DaySchedule>> createStaffSchedule({
required List<DaySchedule> schedules,
});
Future<Map<String, DaySchedule>> updateStaffSchedule({
required List<DaySchedule> schedules,
});
Future<DaySchedule?> deleteStaffSchedule({
required List<DaySchedule> schedules,
});
}

View File

@@ -0,0 +1,141 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/data/enums/state_status.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_app_bar.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_loading_overlay.dart';
import 'package:krow/features/profile/schedule/domain/bloc/schedule_bloc.dart';
import 'package:krow/features/profile/schedule/presentation/widgets/schedule_calendar.dart';
import 'package:krow/features/profile/schedule/presentation/widgets/calendar_button_section_widget.dart';
import 'package:krow/features/profile/schedule/presentation/widgets/calendar_edit_slot.dart';
import 'package:krow/features/profile/schedule/presentation/widgets/calendar_footer_widget.dart';
import 'package:krow/features/profile/schedule/presentation/widgets/calendar_slot_list_widget.dart';
@RoutePage()
class ScheduleScreen extends StatelessWidget implements AutoRouteWrapper {
ScheduleScreen({super.key});
final OverlayPortalController _controller = OverlayPortalController();
bool _calendarBuildWhen(ScheduleState previous, ScheduleState current) {
return previous.isInEditMode != current.isInEditMode ||
previous.isScheduleAddMode != current.isScheduleAddMode ||
previous.selectedDates != current.selectedDates ||
previous.schedules != current.schedules;
}
@override
Widget wrappedRoute(BuildContext context) {
return BlocProvider(
create: (context) => ScheduleBloc()..add(const ScheduleInitEvent()),
child: this,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: KwAppBar(titleText: 'schedule'.tr()),
body: KwLoadingOverlay(
controller: _controller,
child: BlocListener<ScheduleBloc, ScheduleState>(
listenWhen: (previous, current) => previous.status != current.status,
listener: (context, state) {
if (state.status == StateStatus.loading) {
_controller.show();
} else {
_controller.hide();
}
},
child: ListView(
primary: false,
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
children: [
_buildPageTitle(),
const Gap(12),
Container(
decoration: KwBoxDecorations.primaryLight8.copyWith(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
),
child: BlocBuilder<ScheduleBloc, ScheduleState>(
buildWhen: _calendarBuildWhen,
builder: (context, state) {
return ScheduleCalendar(
disabled: state.isInEditMode || state.isScheduleAddMode,
selectedDates: state.selectedDates,
containsScheduleForDate: state.containsScheduleForDate,
onDateSelected: (dates) {
BlocProvider.of<ScheduleBloc>(context).add(
ScheduleEventSelectDates(dates: dates),
);
},
);
},
),
),
BlocBuilder<ScheduleBloc, ScheduleState>(
buildWhen: (previous, current) =>
previous.isInEditMode != current.isInEditMode ||
previous.selectedSchedules != current.selectedSchedules,
builder: (context, state) {
if (state.isInEditMode) {
return const Column(
children: [
CalendarEditSlot(),
CalendarButtonSectionWidget()
],
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
CalendarFooterWidget(
selectedDates: state.selectedDates,
existedSlots: state.selectedSchedules,
),
CalendarSlotListWidget(
selectedSchedules: state.selectedSchedules,
),
],
);
},
),
],
),
),
),
);
}
RichText _buildPageTitle() {
return RichText(
text: TextSpan(
children: [
TextSpan(
text: '${'set_your_availability'.tr()} ',
style: AppTextStyles.bodySmallMed.copyWith(
color: AppColors.blackBlack,
),
),
TextSpan(
text: 'mark_days_and_times'.tr(),
style: AppTextStyles.bodySmallReg.copyWith(
color: AppColors.blackGray,
),
),
],
),
);
}
}

View File

@@ -0,0 +1,41 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
import 'package:krow/features/profile/schedule/domain/bloc/schedule_bloc.dart';
class CalendarButtonSectionWidget extends StatelessWidget {
const CalendarButtonSectionWidget({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
const Gap(16),
BlocSelector<ScheduleBloc, ScheduleState, bool>(
selector: (state) =>
state.tempSchedules.firstOrNull?.isViableForSaving ?? false,
builder: (context, isEnabled) {
return KwButton.primary(
label: 'save_slots'.tr(),
disabled: !isEnabled,
onPressed: () {
BlocProvider.of<ScheduleBloc>(context)
.add(const ScheduleEventSave());
},
);
},
),
const Gap(12),
KwButton.outlinedPrimary(
label: 'cancel'.tr(),
onPressed: () {
BlocProvider.of<ScheduleBloc>(context)
.add(const ScheduleEventCancel());
},
)
],
);
}
}

View File

@@ -0,0 +1,191 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gap/gap.dart';
import 'package:intl/intl.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_popup_menu.dart';
import 'package:krow/features/profile/schedule/domain/bloc/schedule_bloc.dart';
class CalendarDataDetailSettingWidget extends StatefulWidget {
const CalendarDataDetailSettingWidget({
super.key,
required this.selectedDate,
});
final DateTime? selectedDate;
@override
State<CalendarDataDetailSettingWidget> createState() =>
_CalendarDataDetailSettingWidgetState();
}
class _CalendarDataDetailSettingWidgetState
extends State<CalendarDataDetailSettingWidget> {
var mode = SelectionMode.empty;
@override
Widget build(BuildContext context) {
final selectedDate = widget.selectedDate;
if (selectedDate == null) return const SizedBox.shrink();
return Container(
padding: const EdgeInsets.all(12),
decoration: const BoxDecoration(
color: AppColors.graySecondaryFrame,
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(12),
bottomRight: Radius.circular(12),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'selected_date_details'.tr(),
style: AppTextStyles.bodyMediumMed,
),
const Divider(
color: AppColors.grayTintStroke,
),
Row(
children: [
Expanded(
child: Text(
DateFormat('EEEE, MMMM d', context.locale.languageCode).format(selectedDate),
overflow: TextOverflow.ellipsis,
style: AppTextStyles.headingH3,
),
),
const Gap(8),
_buildKwPopupMenu(context, selectedDate),
],
),
const Gap(16),
Row(
children: [
Expanded(
child: KwButton.outlinedPrimary(
label: 'cancel'.tr(),
onPressed: () {
context.read<ScheduleBloc>().add(
const ScheduleEventChangeMode(
isInScheduleAddMode: false,
),
);
},
),
),
const Gap(8),
Expanded(
child: KwButton.primary(
disabled: mode == SelectionMode.empty,
label: 'choose_time'.tr(),
onPressed: () {
BlocProvider.of<ScheduleBloc>(context).add(
ScheduleEventChangeMode(
isInEditScheduleMode: true,
isWeekly: mode == SelectionMode.weekly,
),
);
},
),
),
],
),
],
),
);
}
KwPopupMenu _buildKwPopupMenu(BuildContext context, DateTime selectedDate) {
return KwPopupMenu(
customButtonBuilder: (_, isOpened) {
return _buildPopupButton(isOpened, selectedDate);
},
menuItems: [
KwPopupMenuItem(
icon: Assets.images.icons.calendarV2.svg(
height: 16,
width: 16,
),
title: 'date'.tr(),
onTap: () {
setState(() {
mode = SelectionMode.single;
});
},
),
KwPopupMenuItem(
icon: Assets.images.icons.calendar.svg(
height: 16,
width: 16,
),
title: '${'all'.tr()} ${DateFormat('EEEE', context.locale.languageCode).format(selectedDate)}',
onTap: () {
setState(() {
mode = SelectionMode.weekly;
});
},
)
],
);
}
Widget _buildPopupButton(bool isOpened, DateTime selectedDate) {
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
height: 34,
padding: const EdgeInsets.only(left: 12, right: 10),
decoration: BoxDecoration(
color: AppColors.grayWhite,
borderRadius: BorderRadius.circular(17),
border: Border.all(
color: isOpened ? AppColors.bgColorDark : AppColors.grayTintStroke,
width: 1,
),
),
child: Row(
children: [
if (mode == SelectionMode.empty)
Text(
'modify'.tr(),
style: AppTextStyles.bodySmallMed,
)
else
RichText(
text: TextSpan(
children: [
TextSpan(
text: '${'modify'.tr()}: ',
style: AppTextStyles.bodyMediumMed.copyWith(
color: AppColors.blackGray,
),
),
TextSpan(
text: '${mode == SelectionMode.single ? 'one'.tr() : 'all'.tr()} '
'${DateFormat('EEEE', context.locale.languageCode).format(selectedDate)}',
style: AppTextStyles.bodyMediumMed,
),
],
),
),
const Gap(4),
AnimatedRotation(
duration: const Duration(milliseconds: 150),
turns: isOpened ? -0.5 : 0,
child: Assets.images.icons.caretDown.svg(
height: 16,
width: 16,
),
)
],
),
);
}
}
enum SelectionMode {empty, single, weekly}

View File

@@ -0,0 +1,152 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
import 'package:krow/core/presentation/widgets/kw_time_slot.dart';
import 'package:krow/features/profile/schedule/domain/bloc/schedule_bloc.dart';
import 'package:krow/features/profile/schedule/domain/entities/schedule_slot.dart';
class CalendarEditSlot extends StatelessWidget {
const CalendarEditSlot({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<ScheduleBloc, ScheduleState>(
buildWhen: (previous, current) {
return previous.scheduleErrorText != current.scheduleErrorText ||
previous.tempSchedules != current.tempSchedules;
},
builder: (context, state) {
final daySchedule = state.tempSchedules.first;
return Container(
padding: const EdgeInsets.all(12),
decoration: const BoxDecoration(
color: AppColors.graySecondaryFrame,
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(12),
bottomRight: Radius.circular(12),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'what_hours_available'.tr(),
style: AppTextStyles.bodyMediumMed,
),
const Divider(
color: AppColors.grayTintStroke,
),
AnimatedSize(
duration: const Duration(milliseconds: 300),
alignment: Alignment.topCenter,
child: Column(
children: [
for (int i = 0; i < daySchedule.slots.length; i++)
_CalendarEditSlotItemWidget(
slot: daySchedule.slots[i],
slotIndex: i,
),
],
),
),
if (state.scheduleErrorText.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Text(
state.scheduleErrorText,
style: AppTextStyles.bodyTinyReg.copyWith(
color: AppColors.statusError,
),
),
),
const Gap(16),
KwButton.secondary(
label: 'add_slot'.tr(),
leftIcon: Assets.images.icons.add,
onPressed: () {
BlocProvider.of<ScheduleBloc>(context).add(
const ScheduleEventAddSlot(),
);
},
)
],
),
);
},
);
}
}
class _CalendarEditSlotItemWidget extends StatelessWidget {
const _CalendarEditSlotItemWidget({
required this.slot,
required this.slotIndex,
});
final ScheduleSlot slot;
final int slotIndex;
@override
Widget build(BuildContext context) {
return Column(
children: [
const Gap(8),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: KwTimeSlotInput(
label: 'start_time'.tr(),
initialValue: slot.startTime,
onChange: (DateTime start) {
BlocProvider.of<ScheduleBloc>(context).add(
ScheduleEventEditSlot(
start: start,
slot: slot,
slotIndex: slotIndex,
),
);
},
),
),
const Gap(8),
Expanded(
child: KwTimeSlotInput(
label: 'end_time'.tr(),
initialValue: slot.endTime,
onChange: (DateTime end) {
BlocProvider.of<ScheduleBloc>(context).add(
ScheduleEventEditSlot(
end: end,
slot: slot,
slotIndex: slotIndex,
),
);
},
),
),
const Gap(8),
KwButton.secondary(
onPressed: () {
BlocProvider.of<ScheduleBloc>(context).add(
ScheduleEventRemoveSlot(
slot: slot,
slotIndex: slotIndex,
),
);
},
leftIcon: Assets.images.icons.x,
fit: KwButtonFit.circular,
)
],
),
],
);
}
}

View File

@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow/features/profile/schedule/domain/entities/day_shedule.dart';
import 'package:krow/features/profile/schedule/domain/bloc/schedule_bloc.dart';
import 'package:krow/features/profile/schedule/presentation/widgets/calendar_data_detail_setting_widget.dart';
import 'package:krow/features/profile/schedule/presentation/widgets/calendar_legend_widget.dart';
class CalendarFooterWidget extends StatelessWidget {
const CalendarFooterWidget({
super.key,
required this.selectedDates,
required this.existedSlots,
});
final List<DateTime> selectedDates;
final List<DaySchedule> existedSlots;
@override
Widget build(BuildContext context) {
return BlocSelector<ScheduleBloc, ScheduleState, bool>(
selector: (state) => state.isScheduleAddMode,
builder: (context, isInScheduleAddMode) {
return AnimatedCrossFade(
duration: Durations.short4,
crossFadeState: isInScheduleAddMode
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
firstChild: CalendarLegendWidget(
selectedDates: selectedDates,
existedSlots: existedSlots,
),
secondChild: CalendarDataDetailSettingWidget(
selectedDate: selectedDates.firstOrNull,
),
);
},
);
}
}

View File

@@ -0,0 +1,117 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
import 'package:krow/features/profile/schedule/domain/entities/day_shedule.dart';
import 'package:krow/features/profile/schedule/presentation/widgets/shared_functions.dart';
class CalendarLegendWidget extends StatelessWidget {
const CalendarLegendWidget({
super.key,
required this.selectedDates,
required this.existedSlots,
});
final List<DateTime> selectedDates;
final List<DaySchedule> existedSlots;
@override
Widget build(BuildContext context) {
final selectedDatesOption = _getSelectedDatesOption();
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: const BoxDecoration(
color: AppColors.grayPrimaryFrame,
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(12),
bottomRight: Radius.circular(12),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Divider(
color: AppColors.grayTintStroke,
),
const Gap(8),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(
height: 16,
width: 16,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: AppColors.bgColorDark,
),
),
const Gap(8),
Text(
'available'.tr(),
style: AppTextStyles.bodyMediumReg,
),
const Gap(32),
Container(
height: 16,
width: 16,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: AppColors.blackCaptionText,
),
),
const Gap(8),
Text(
'not_available'.tr(),
style: AppTextStyles.bodyMediumReg,
),
const Spacer(),
if (selectedDates.isEmpty)
const SizedBox(
height: 34,
)
else
Container(
decoration: BoxDecoration(
border: Border.all(
color: AppColors.grayTintStroke,
),
borderRadius: BorderRadius.circular(16),
),
child: KwButton.secondary(
height: 32,
label: switch (selectedDatesOption) {
SelectedDatesOption.add => 'add'.tr(),
SelectedDatesOption.edit => 'edit'.tr(),
SelectedDatesOption.editAll => 'edit_all'.tr(),
},
onPressed: () => onEditButtonPress(
context,
selectedDatesOption: selectedDatesOption,
isWeeklySchedule: existedSlots.length == 1 &&
existedSlots.first.isWeekly,
),
rightIcon: existedSlots.isNotEmpty
? Assets.images.icons.edit
: Assets.images.icons.add,
),
),
],
),
const Gap(24),
],
),
);
}
SelectedDatesOption _getSelectedDatesOption() {
if (existedSlots.isEmpty) return SelectedDatesOption.add;
return existedSlots.length > 1 || selectedDates.length > 1
? SelectedDatesOption.editAll
: SelectedDatesOption.edit;
}
}

View File

@@ -0,0 +1,184 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gap/gap.dart';
import 'package:intl/intl.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/core/presentation/widgets/ui_kit/dialogs/kw_dialog.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_popup_menu.dart';
import 'package:krow/features/profile/schedule/domain/entities/day_shedule.dart';
import 'package:krow/features/profile/schedule/domain/bloc/schedule_bloc.dart';
import 'package:krow/features/profile/schedule/domain/entities/schedule_slot.dart';
import 'package:krow/features/profile/schedule/presentation/widgets/shared_functions.dart';
import '../../../../../app.dart';
class CalendarSlotListWidget extends StatelessWidget {
const CalendarSlotListWidget({
super.key,
required this.selectedSchedules,
});
final List<DaySchedule> selectedSchedules;
static final timeFormat = DateFormat('h:mma', 'en');
Future<void> _onDeleteButtonPress(
BuildContext context,
DaySchedule schedule,
) async {
final scheduleBloc = context.read<ScheduleBloc>();
if (!schedule.isWeekly) {
scheduleBloc.add(
ScheduleEventDeleteSchedule(schedule: schedule),
);
return;
}
await KwDialog.show(
context: context,
state: KwDialogState.warning,
icon: Assets.images.icons.alertTriangle,
title: 'delete_schedule'.tr(),
message: 'delete_schedule_message'.tr(args: [(DateFormat('EEEE', context.locale.languageCode).format(schedule.date))]),
primaryButtonLabel: 'confirm'.tr(),
secondaryButtonLabel: 'cancel'.tr(),
onPrimaryButtonPressed: (dialogContext) {
scheduleBloc.add(
ScheduleEventDeleteSchedule(schedule: schedule),
);
Navigator.of(dialogContext).pop();
},
);
}
@override
Widget build(BuildContext context) {
return BlocBuilder<ScheduleBloc, ScheduleState>(
buildWhen: (previous, current) =>
previous.selectedDates != current.selectedDates,
builder: (context, state) {
if (state.selectedDates.isEmpty) return const SizedBox.shrink();
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Gap(12),
Text(
'slot_details'.tr(),
style: AppTextStyles.bodySmallMed,
),
const Gap(12),
for (var schedule in selectedSchedules)
_buildInfoWidget(context, schedule),
],
);
},
);
}
Widget _buildInfoWidget(context, DaySchedule schedule) {
return Container(
margin: const EdgeInsets.only(top: 12),
padding: const EdgeInsets.all(12),
decoration: KwBoxDecorations.primaryLight8,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Container(
height: 36,
width: 36,
decoration: BoxDecoration(
color: AppColors.grayWhite,
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: AppColors.grayStroke,
width: 1,
),
),
child: Center(
child: Assets.images.icons.calendar.svg(),
),
),
const Gap(12),
Text(
DateFormat('MMMM d', appContext!.locale.languageCode).format(schedule.date),
style: AppTextStyles.headingH3,
),
const Spacer(),
KwPopupMenu(
horizontalMargin: 24,
menuItems: [
KwPopupMenuItem(
title: 'edit'.tr(),
icon: Assets.images.icons.edit.svg(),
onTap: () => onEditButtonPress(
context,
editedDate: schedule.date,
isWeeklySchedule: schedule.isWeekly,
),
),
KwPopupMenuItem(
title: 'delete'.tr(),
textStyle: AppTextStyles.bodyMediumReg.copyWith(
color: AppColors.statusError,
),
icon: Assets.images.icons.trash.svg(),
onTap: () => _onDeleteButtonPress(context, schedule),
),
],
),
],
),
const Gap(12),
Text(
'time_slots'.tr(),
style: AppTextStyles.bodySmallMed.copyWith(
color: AppColors.blackGray,
),
),
for (var i = 0; i < schedule.slots.length / 2; i++)
Row(
children: [
_buildSlotWidget(context, schedule.slots[i * 2]),
const Gap(12),
schedule.slots.length > (i * 2 + 1)
? _buildSlotWidget(context, schedule.slots[i * 2 + 1])
: const Spacer(),
],
),
],
),
);
}
_buildSlotWidget(BuildContext context, ScheduleSlot slot) {
return Expanded(
child: Container(
margin: const EdgeInsets.only(top: 12),
height: 36,
decoration: BoxDecoration(
color: AppColors.grayWhite,
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: AppColors.grayTintStroke,
width: 1,
),
),
child: Center(
child: Text(
'${timeFormat.format(slot.startTime)} - '
'${timeFormat.format(slot.endTime)}',
style: AppTextStyles.bodyMediumReg,
),
),
),
);
}
}

View File

@@ -0,0 +1,235 @@
import 'package:calendar_date_picker2/calendar_date_picker2.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
class ScheduleCalendar extends StatefulWidget {
const ScheduleCalendar({
super.key,
required this.selectedDates,
required this.onDateSelected,
required this.containsScheduleForDate,
required this.disabled,
});
// static const _monthNames = [
// 'Jan',
// 'Feb',
// 'Mar',
// 'Apr',
// 'May',
// 'Jun',
// 'Jul',
// 'Aug',
// 'Sep',
// 'Oct',
// 'Nov',
// 'Dec',
// ];
final List<DateTime> selectedDates;
final void Function(List<DateTime> value) onDateSelected;
final bool Function(DateTime date) containsScheduleForDate;
final bool disabled;
@override
State<ScheduleCalendar> createState() => _ScheduleCalendarState();
}
class _ScheduleCalendarState extends State<ScheduleCalendar> {
final currentDate = DateTime.now();
final selectedTextStyle = AppTextStyles.bodyMediumSmb.copyWith(
color: AppColors.grayWhite,
);
final dayTextStyle = AppTextStyles.bodyMediumReg.copyWith(
color: AppColors.bgColorDark,
);
late List<DateTime> _selectedDates =
_getStateSelectedDates(widget.selectedDates);
List<DateTime> _getStateSelectedDates(List<DateTime> selectedDates) {
if (selectedDates.isEmpty) return [];
return [
selectedDates.first,
if (selectedDates.length > 1) selectedDates.last,
];
}
@override
void didUpdateWidget(covariant ScheduleCalendar oldWidget) {
_selectedDates = _getStateSelectedDates(widget.selectedDates);
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
return IgnorePointer(
ignoring: widget.disabled,
child: CalendarDatePicker2(
value: _selectedDates,
config: CalendarDatePicker2Config(
calendarType: CalendarDatePicker2Type.range,
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(),
lastMonthIcon: Assets.images.icons.caretLeft.svg(),
customModePickerIcon: Padding(
padding: const EdgeInsets.only(left: 4),
child: Assets.images.icons.caretDown.svg(),
),
// modePickerBuilder: _controlBuilder,
),
onValueChanged: widget.onDateSelected,
),
);
}
Widget? _monthBuilder({
required int month,
TextStyle? textStyle,
BoxDecoration? decoration,
bool? isSelected,
bool? isDisabled,
bool? isCurrentMonth,
}) {
return Center(
child: Container(
margin: const EdgeInsets.only(top: 12),
height: 46,
decoration: BoxDecoration(
color: isSelected == true ? AppColors.blackDarkBgBody : null,
borderRadius: BorderRadius.circular(23),
border: Border.all(
color: AppColors.grayStroke,
width: isSelected == true ? 0 : 1,
),
),
child: Center(
child: Text(
DateFormat.MMM(context.locale.toLanguageTag()).format(
DateFormat('M').parse(month.toString()),
),
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 Center(
child: Text(
year.toString(),
style: dayTextStyle,
textAlign: TextAlign.center,
),
);
}
Widget? _dayBuilder({
required DateTime date,
TextStyle? textStyle,
BoxDecoration? decoration,
bool? isSelected,
bool? isDisabled,
bool? isToday,
}) {
final eventExist = widget.containsScheduleForDate(date);
return Center(
child: Container(
margin: const EdgeInsets.only(left: 2, right: 2),
alignment: Alignment.center,
decoration: BoxDecoration(
color: isSelected == true ? AppColors.bgColorDark : null,
borderRadius: BorderRadius.circular(20),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (eventExist) const Gap(5),
Text(
date.day.toString(),
style: isSelected == true
? selectedTextStyle
: AppTextStyles.bodyMediumReg.copyWith(
color: _isPast(date)
? AppColors.blackCaptionText
: AppColors.bgColorDark,
),
textAlign: TextAlign.center,
),
if (eventExist) ...[
const Gap(1),
DecoratedBox(
decoration: BoxDecoration(
color: isSelected == true
? AppColors.grayWhite
: AppColors.bgColorDark,
borderRadius: BorderRadius.circular(2),
),
child: const SizedBox(
height: 4,
width: 4,
),
),
],
const Gap(2),
],
),
),
);
}
bool _isPast(DateTime date) {
return date.isBefore(
DateTime(currentDate.year, currentDate.month, currentDate.day),
);
}
Widget? _dayWeekBuilder({
required int weekday,
bool? isScrollViewTopHeader,
}) {
print(DateFormat('d', 'en').parse((weekday).toString()));
return Text(
DateFormat.E(context.locale.toLanguageTag()).format(
DateTime(2020, 1, (5 + weekday)) // 5 is Monday, so we start from there),
),
style: AppTextStyles.bodyMediumSmb.copyWith(
color: AppColors.bgColorDark,
),
textAlign: TextAlign.center,
);
}
}

View File

@@ -0,0 +1,63 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/widgets/ui_kit/dialogs/kw_dialog.dart';
import 'package:krow/features/profile/schedule/domain/bloc/schedule_bloc.dart';
import 'package:krow/features/profile/schedule/presentation/widgets/weekly_option_selector.dart';
enum SelectedDatesOption { add, edit, editAll }
Future<void> onEditButtonPress(
BuildContext context, {
required bool isWeeklySchedule,
SelectedDatesOption selectedDatesOption = SelectedDatesOption.edit,
DateTime? editedDate,
}) async {
if (selectedDatesOption != SelectedDatesOption.edit) {
context.read<ScheduleBloc>().add(
const ScheduleEventChangeMode(
isInScheduleAddMode: true,
isInEditScheduleMode: true,
),
);
return;
}
if (!isWeeklySchedule) {
context.read<ScheduleBloc>().add(
ScheduleEventChangeMode(
isWeekly: false,
isInEditScheduleMode: true,
editedDate: editedDate,
),
);
return;
}
var isWeeklySelected = false;
await KwDialog.show(
context: context,
icon: Assets.images.icons.menuBoard,
title: 'update_schedule'.tr(),
message: 'update_schedule_message'.tr(),
primaryButtonLabel: 'submit_update'.tr(),
secondaryButtonLabel: 'cancel'.tr(),
onPrimaryButtonPressed: (dialogContext) {
context.read<ScheduleBloc>().add(
ScheduleEventChangeMode(
isWeekly: isWeeklySelected,
isInEditScheduleMode: true,
editedDate: editedDate,
),
);
Navigator.of(dialogContext).pop();
},
child: WeeklyOptionSelector(
onWeeklyToggle: (isWeekly) {
isWeeklySelected = isWeekly;
},
),
);
}

View File

@@ -0,0 +1,36 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_option_selector.dart';
class WeeklyOptionSelector extends StatefulWidget {
const WeeklyOptionSelector({
super.key,
required this.onWeeklyToggle,
});
final void Function(bool) onWeeklyToggle;
@override
State<WeeklyOptionSelector> createState() => _WeeklyOptionSelectorState();
}
class _WeeklyOptionSelectorState extends State<WeeklyOptionSelector> {
int selectedOption = 0;
@override
Widget build(BuildContext context) {
return KwOptionSelector(
selectedIndex: selectedOption,
selectedColor: AppColors.blackDarkBgBody,
itemBorder: const Border.fromBorderSide(
BorderSide(color: AppColors.grayStroke),
),
onChanged: (index) => setState(() {
selectedOption = index;
widget.onWeeklyToggle(selectedOption != 0);
}),
items: ['this_day_only'.tr(), 'entire_schedule'.tr()],
);
}
}