new project stable version

This commit is contained in:
2026-05-10 19:11:31 +03:00
commit 3616f84556
391 changed files with 23857 additions and 0 deletions

View File

@@ -0,0 +1,209 @@
import 'dart:async';
import 'package:be_happy/domain/entities/active_scooter_order.dart';
import 'package:be_happy/domain/usecase/update_scooter_order_data_usecase.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/result.dart';
import '../../../domain/entities/scooter_order.dart';
import '../../../domain/usecase/finish_ride_usecase.dart';
import '../../../domain/usecase/pause_ride_usecase.dart';
import '../../../domain/usecase/resume_ride_usecase.dart';
import '../../../domain/usecase/get_scooter_order_by_id_usecase.dart';
import '../event/active_ride_event.dart';
import '../state/active_ride_state.dart';
class ActiveRideBloc extends Bloc<ActiveRideEvent, ActiveRideState> {
final FinishRideUsecase _finishRideUsecase;
final PauseRideUsecase _pauseRideUsecase;
final ResumeRideUsecase _resumeRideUsecase;
final GetScooterOrderByIdUsecase _getScooterOrderByIdUsecase;
final UpdateScooterOrderDataUsecase _updateScooterOrderDataUsecase;
Timer? _syncTimer;
ActiveRideBloc(
this._finishRideUsecase,
this._pauseRideUsecase,
this._resumeRideUsecase,
this._getScooterOrderByIdUsecase,
this._updateScooterOrderDataUsecase,
) : super(const ActiveRideState()) {
on<LoadScooterOrder>(_onLoadScooterOrder);
on<PauseRide>(_onPauseRide);
on<ResumeRide>(_onResumeRide);
on<FinishRide>(_onFinishRide);
on<SyncScooterOrder>(_onSyncScooterOrder);
}
@override
Future<void> close() {
_syncTimer?.cancel();
return super.close();
}
Future<void> _onLoadScooterOrder(
LoadScooterOrder event,
Emitter<ActiveRideState> emit,
) async {
emit(state.copyWith(status: ActiveRideStatus.loading));
final results = await Future.wait([
_getScooterOrderByIdUsecase(event.orderId),
_updateScooterOrderDataUsecase(orderId: event.orderId),
]);
final orderResult = results[0];
final activeOrderData = results[1];
if (orderResult is Success<ScooterOrder>) {
final order = orderResult.data;
// Пытаемся достать доп. данные, если они Success
final orderData = activeOrderData is Success<ActiveScooterOrder>
? activeOrderData.data
: null;
final startTime = order?.startAt ?? order?.createdAt;
final elapsedTime = DateTime.now().difference(startTime!);
final isPaused = order?.status.toLowerCase() == 'pause';
print("ORDER DATA2: $orderData");
emit(state.copyWith(
status: ActiveRideStatus.success,
order: order,
elapsedTime: elapsedTime,
speed: orderData?.speed ?? 0.0,
distance: orderData?.mileage ?? 0.0,
cost: orderData?.price ?? 0.0,
isPaused: isPaused,
inZone: orderData?.zone,
));
//synchronize
_syncTimer?.cancel();
_syncTimer = Timer.periodic(const Duration(seconds: 5), (timer) {
add(SyncScooterOrder(event.orderId));
});
} else if (orderResult is Failure) {
emit(state.copyWith(
status: ActiveRideStatus.failure,
errorMessage: 'Не удалось загрузить информацию о поездке',
));
}
print("CURRENT STATE $state");
}
Future<void> _onPauseRide(
PauseRide event,
Emitter<ActiveRideState> emit,
) async {
emit(state.copyWith(status: ActiveRideStatus.loading));
final result = await _pauseRideUsecase(event.orderId);
if (result is Success<ScooterOrder>) {
emit(state.copyWith(
status: ActiveRideStatus.success,
order: result.data,
isPaused: true,
));
} else if (result is Failure) {
emit(state.copyWith(
status: ActiveRideStatus.failure,
errorMessage: 'Не удалось поставить поездку на паузу',
));
}
print("CURRENT STATE $state");
}
Future<void> _onResumeRide(
ResumeRide event,
Emitter<ActiveRideState> emit,
) async {
emit(state.copyWith(status: ActiveRideStatus.loading));
final result = await _resumeRideUsecase(event.orderId);
if (result is Success<ScooterOrder>) {
emit(state.copyWith(
status: ActiveRideStatus.success,
order: result.data,
isPaused: false,
));
} else if (result is Failure) {
emit(state.copyWith(
status: ActiveRideStatus.failure,
errorMessage: 'Не удалось возобновить поездку',
));
}
print("CURRENT STATE $state");
}
Future<void> _onFinishRide(
FinishRide event,
Emitter<ActiveRideState> emit,
) async {
// emit(state.copyWith(status: ActiveRideStatus.loading));
/*final result = await _finishRideUsecase(event.orderId);
if (result is Success<ScooterOrder>) {
_syncTimer?.cancel();
_syncTimer = null;
emit(state.copyWith(
status: ActiveRideStatus.success,
order: result.data,
));
} else if (result is Failure) {
emit(state.copyWith(
status: ActiveRideStatus.failure,
errorMessage: 'Не удалось завершить поездку',
));
}*/
}
Future<void> _onSyncScooterOrder(
SyncScooterOrder event,
Emitter<ActiveRideState> emit,
) async {
final results = await Future.wait([
_getScooterOrderByIdUsecase(event.orderId),
_updateScooterOrderDataUsecase(orderId: event.orderId),
]);
final orderResult = results[0];
final activeOrderData = results[1];
if (orderResult is Success<ScooterOrder>) {
final order = orderResult.data;
// Пытаемся достать доп. данные, если они Success
final orderData = activeOrderData is Success<ActiveScooterOrder>
? activeOrderData.data
: null;
final startTime = order?.startAt ?? order?.createdAt;
final elapsedTime = DateTime.now().difference(startTime!);
final isPaused = order?.status.toLowerCase() == 'pause';
emit(state.copyWith(
order: order,
elapsedTime: elapsedTime,
speed: orderData?.speed ?? 0.0,
distance: orderData?.mileage ?? 0.0,
cost: orderData?.price ?? state.cost,
isPaused: isPaused,
inZone: orderData?.zone,
));
}
print("CURRENT STATE $state");
}
}

View File

@@ -0,0 +1,112 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../domain/usecase/add_payment_card_usecase.dart';
import '../event/add_card_event.dart';
import '../state/add_card_state.dart';
// 🔹 Импорты для Result и Failure
import '../../core/result.dart';
import '../../core/failures.dart';
class AddCardBloc extends Bloc<AddCardEvent, AddCardState> {
final AddPaymentCardUsecase addPaymentCardUsecase;
AddCardBloc(this.addPaymentCardUsecase) : super(const AddCardState()) {
on<CardNumberChanged>(_onCardNumberChanged);
on<ExpiryDateChanged>(_onExpiryDateChanged);
on<CvvChanged>(_onCvvChanged);
on<CardHolderChanged>(_onCardHolderChanged);
on<AddCardSubmitted>(_onAddCardSubmitted);
}
void _onCardNumberChanged(CardNumberChanged event, Emitter<AddCardState> emit) {
final formatted = _formatCardNumber(event.cardNumber);
emit(state.copyWith(cardNumber: formatted));
}
void _onExpiryDateChanged(ExpiryDateChanged event, Emitter<AddCardState> emit) {
final formatted = _formatExpiryDate(event.expiryDate);
emit(state.copyWith(expiryDate: formatted));
}
void _onCvvChanged(CvvChanged event, Emitter<AddCardState> emit) {
final formatted = event.cvv.replaceAll(RegExp(r'\D'), '').substring(0, 3);
emit(state.copyWith(cvv: formatted));
}
void _onCardHolderChanged(CardHolderChanged event, Emitter<AddCardState> emit) {
emit(state.copyWith(cardHolder: event.cardHolder));
}
Future<void> _onAddCardSubmitted(AddCardSubmitted event, Emitter<AddCardState> emit) async {
if (state.cardNumber.isEmpty || state.expiryDate.isEmpty) return;
emit(state.copyWith(status: AddCardStatus.loading));
final expiryParts = state.expiryDate.split('/'); // Используем state
if (expiryParts.length != 2) {
emit(state.copyWith(
status: AddCardStatus.failure,
errorMessage: 'Неверный формат даты',
));
return;
}
final result = await addPaymentCardUsecase(
cardNumber: state.cardNumber.replaceAll(' ', ''),
expiryMonth: expiryParts[0],
expiryYear: expiryParts[1],
cardHolder: state.cardHolder.trim(),
cvv: state.cvv,
);
if (result is Success) {
emit(state.copyWith(status: AddCardStatus.success));
} else if (result is Failure) {
final failure = result.failure;
String errorMessage = 'Ошибка добавления карты';
if (failure is AuthFailure) {
errorMessage = 'Неверные данные карты';
} else if (failure is AuthBlockFailure) {
errorMessage = 'Доступ заблокирован';
} else if (failure is UnknownFailure) {
errorMessage = failure.message ?? 'Неизвестная ошибка';
}
emit(state.copyWith(
status: AddCardStatus.failure,
errorMessage: errorMessage,
));
}
}
String _formatCardNumber(String value) {
final cleaned = value.replaceAll(RegExp(r'\D'), '');
final limited = cleaned.substring(0, cleaned.length > 16 ? 16 : cleaned.length);
String formatted = '';
for (int i = 0; i < limited.length; i++) {
if (i > 0 && i % 4 == 0) {
formatted += ' ';
}
formatted += limited[i];
}
return formatted;
}
String _formatExpiryDate(String value) {
final cleaned = value.replaceAll(RegExp(r'\D'), '');
if (cleaned.isEmpty) return '';
if (cleaned.length <= 2) {
return cleaned;
}
final month = cleaned.substring(0, 2);
final year = cleaned.substring(2, cleaned.length > 4 ? 4 : cleaned.length);
return '$month/$year';
}
}

View File

@@ -0,0 +1,51 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../domain/usecase/login_usecase.dart';
import '../event/auth_event.dart';
import '../state/auth_state.dart';
class PhoneAuthBloc extends Bloc<PhoneAuthEvent, PhoneAuthState> {
final LoginUseCase _loginUseCase;
PhoneAuthBloc(this._loginUseCase) : super(PhoneAuthState.initial()) {
on<PhoneChanged>((event, emit) {
emit(state.copyWith(phone: event.phone));
});
on<IsAdultChanged>((event, emit) {
emit(state.copyWith(isAdult: event.isAdult));
});
on<PrivacyAcceptedChanged>((event, emit) {
emit(state.copyWith(privacyAccepted: event.accepted));
});
on<SubmitPhonePressed>(_onSubmitPhonePressed);
}
Future<void> _onSubmitPhonePressed(
SubmitPhonePressed event,
Emitter<PhoneAuthState> emit,
) async {
if (state.isSubmitting) return;
final phone = state.phone;
final isAdult = state.isAdult;
final privacyAccepted = state.privacyAccepted;
if (phone.isEmpty || !isAdult || !privacyAccepted) {
emit(state.copyWith(error: "Пожалуйста, заполните все поля."));
return;
}
emit(state.copyWith(isSubmitting: true, error: null));
//готовы отправить данные, вызываем юзкейс
try {
final tempToken = await _loginUseCase.execute('+375$phone');
print("TOKEN ON PRESENTATION LAYER: $tempToken");
emit(state.copyWith(isSubmitting: false, isSuccess: true));
} catch (e) {
emit(state.copyWith(isSubmitting: false, error: e.toString()));
}
}
}

View File

@@ -0,0 +1,40 @@
import 'package:be_happy/domain/usecase/get_profile_usecase.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../core/result.dart';
import '../../domain/entities/scooter_order.dart';
import '../../domain/usecase/get_client_orders_usecase.dart';
import '../event/current_rides_event.dart';
import '../state/current_rides_state.dart';
class CurrentRidesBloc extends Bloc<CurrentRidesEvent, CurrentRidesState> {
final GetClientOrdersUsecase _getClientOrdersUsecase;
CurrentRidesBloc(this._getClientOrdersUsecase) : super(const CurrentRidesState()) {
on<LoadClientOrders>(_onLoadClientOrders);
}
Future<void> _onLoadClientOrders(
LoadClientOrders event,
Emitter<CurrentRidesState> emit,
) async {
emit(state.copyWith(status: CurrentRidesStatus.loading));
final result = await _getClientOrdersUsecase();
if (result is Success<List<ScooterOrder>>) {
print("RESULT: ${result.data}");
emit(state.copyWith(
status: CurrentRidesStatus.success,
orders: result.data ?? [],
));
} else if (result is Failure) {
emit(state.copyWith(
status: CurrentRidesStatus.failure,
errorMessage: "Не удалось загрузить заказы",
));
}
}
}

View File

@@ -0,0 +1,59 @@
import 'package:be_happy/domain/entities/user_profile.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import '../../domain/usecase/get_profile_usecase.dart';
import '../../domain/usecase/update_profile_usecase.dart';
import '../event/edit_profile_event.dart';
import '../event/profile_event.dart';
import '../state/edit_profile_state.dart';
import '../state/profile_state.dart';
class EditProfileBloc
extends Bloc<EditProfileEvent, EditProfileState> {
final UpdateProfileUseCase updateProfileUseCase;
final GetProfileUseCase getProfileUseCase;
EditProfileBloc(this.updateProfileUseCase, this.getProfileUseCase)
: super(EditProfileState.initial()) {
on<EditProfileStarted>(_onStarted);
on<EditProfileSubmitted>(_onSubmitted);
}
Future<void> _onSubmitted(
EditProfileSubmitted event,
Emitter<EditProfileState> emit,
) async {
emit(state.copyWith(isSaving: true));
try {
final result = await updateProfileUseCase(event.profile);
if (result != null) {
emit(state.copyWith(isSaving: false, isSuccess: true));
} else {
emit(state.copyWith(isSaving: false, error: "Не удалось обновить профиль"));
}
} catch (e) {
emit(state.copyWith(isSaving: false, error: e.toString()));
}
}
Future<void> _onStarted(
EditProfileStarted event,
Emitter<EditProfileState> emit,
) async {
emit(state.copyWith(isSaving: true));
print("EDIT BLOC STARTED");
try {
final profile = await getProfileUseCase();
emit(state.copyWith(profile: profile, isSaving: false, isSuccess: false));
} catch (e) {
emit(state.copyWith(isSaving: false, error: e.toString()));
}
}
}

View File

@@ -0,0 +1,283 @@
import 'dart:async';
import 'package:be_happy/core/result.dart';
import 'package:be_happy/domain/entities/client_notification.dart';
import 'package:be_happy/domain/entities/map_settings.dart';
import 'package:be_happy/domain/usecase/check_user_usecase.dart';
import 'package:be_happy/domain/usecase/get_available_zones_usecase.dart';
import 'package:be_happy/domain/usecase/get_map_settings_usecase.dart';
import 'package:be_happy/domain/usecase/get_notifications_stream_usecase.dart';
import 'package:be_happy/domain/usecase/logout_usecase.dart';
import 'package:be_happy/presentation/viewmodel/splash_bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../domain/entities/point.dart';
import '../../domain/entities/scooter.dart';
import '../../domain/entities/zone.dart';
import '../../domain/usecase/get_available_scooters_usecase.dart';
import '../../domain/usecase/get_profile_usecase.dart';
import '../event/map_event.dart';
import '../event/spalsh_event.dart';
import '../state/map_state.dart';
import 'package:maps_toolkit/maps_toolkit.dart' as mt;
class MapBloc extends Bloc<ScooterEvent, ScooterState> {
final GetAvailableScootersUsecase getScootersUsecase;
final GetAvailableZonesUsecase getAvailableZonesUsecase;
final GetMapSettingsUsecase getMapSettingsUsecase;
final GetNotificationsStreamUseCase getNotificationsStreamUseCase;
final GetProfileUseCase getProfileUseCase;
final CheckUserUseCase checkUserUseCase;
final LogoutUseCase logoutUseCase;
final SplashBloc splashBloc;
StreamSubscription<ClientNotification>? _notificationSubscription;
MapBloc(
this.getAvailableZonesUsecase,
this.getScootersUsecase,
this.getMapSettingsUsecase,
this.getNotificationsStreamUseCase,
this.getProfileUseCase,
this.checkUserUseCase,
this.logoutUseCase,
this.splashBloc,
) : super(ScooterState(isGeomarksShowed: true)) {
on<FetchScooters>(_onFetchScooters);
on<UpdateMap>(_onUpdateMap);
on<UpdateUserLocation>(_onUpdateUserLocation);
on<NotificationReceived>(_onNotificationReceived);
on<FetchProfileData>(_onFetchProfileData);
on<CheckUser>(_onCheckUser);
on<LogoutPressed>(_onLogoutPressed);
}
void startNotificationStream() {
_notificationSubscription?.cancel();
_notificationSubscription = getNotificationsStreamUseCase().listen(
(notification) {
add(NotificationReceived(notification));
},
onError: (error) {
print(" SSE BLOC ERROR: $error");
_handleReconnect();
},
onDone: () {
print(" SSE Stream closed by server (onDone)");
_handleReconnect();
},
cancelOnError: true,
);
}
void _handleReconnect() {
if (isClosed) return;
Future.delayed(const Duration(seconds: 5), () {
if (!isClosed) {
print(" Attempting to reconnect to SSE...");
startNotificationStream();
}
});
}
void stopNotificationStream() {
_notificationSubscription?.cancel();
_notificationSubscription = null;
}
Future<void> _onFetchScooters(
FetchScooters event,
Emitter<ScooterState> emit,
) async {
emit(state.copyWith(status: ScooterStatus.loading));
try {
final results = await Future.wait([
getScootersUsecase(event.areaScooters, 0, 100),
getAvailableZonesUsecase(event.area, 0, 100),
getMapSettingsUsecase(),
]);
final scooters = results[0] as List<Scooter>;
final zones = results[1] as List<Zone>;
final settings = results[2] as MapSettings;
zones.forEach(print);
List<Zone> filteredZones = [];
if (settings.all_zones) {
if (settings.parking_zones) {
filteredZones.addAll(zones.where((el) => el.type == "Finish"));
}
if (settings.restricted_parking_zones) {
filteredZones.addAll(zones.where((el) => el.type == "Drive"));
}
if (settings.restricted_driving_zones) {
filteredZones.addAll(zones.where((el) => el.type == "NotDrive"));
}
}
print(
" FETCH: filteredZones.length = ${filteredZones.length}, all_zones=${settings.all_zones}",
);
emit(
state.copyWith(
status: ScooterStatus.success,
scooters: scooters,
zones: filteredZones,
area: event.area,
areaScooters: event.areaScooters,
isGeomarksShowed: settings.all_placemarks,
),
);
} catch (e) {
print("FETCH ERROR: $e");
emit(
state.copyWith(
status: ScooterStatus.failure,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onUpdateMap(UpdateMap event, Emitter<ScooterState> emit) async {
emit(state.copyWith(status: ScooterStatus.loading));
try {
final results = await Future.wait([
getScootersUsecase(state.areaScooters, 0, 100),
getAvailableZonesUsecase(state.area, 0, 100),
getMapSettingsUsecase(),
]);
final scooters = results[0] as List<Scooter>;
final zones = results[1] as List<Zone>;
final settings = results[2] as MapSettings;
List<Zone> filteredZones = [];
if (settings.all_zones) {
if (settings.parking_zones) {
filteredZones.addAll(zones.where((el) => el.type == "Finish"));
}
if (settings.restricted_parking_zones) {
filteredZones.addAll(zones.where((el) => el.type == "Drive"));
}
if (settings.restricted_driving_zones) {
filteredZones.addAll(zones.where((el) => el.type == "NotDrive"));
}
}
print(
" UPDATE MAP: filteredZones.length = ${filteredZones.length}, all_zones=${settings.all_zones}",
);
final zonesToEmit = filteredZones.isNotEmpty
? filteredZones
: state.zones;
emit(
state.copyWith(
status: ScooterStatus.success,
scooters: scooters,
zones: zonesToEmit,
isGeomarksShowed: settings.all_placemarks,
),
);
} catch (e) {
print("UPDATE ERROR: $e");
emit(
state.copyWith(
status: ScooterStatus.failure,
errorMessage: e.toString(),
),
);
}
}
FutureOr<void> _onUpdateUserLocation(
UpdateUserLocation event,
Emitter<ScooterState> emit,
) {
print("USER LOCATION UPDATED EVENT start");
state.zones.forEach((z) {
if (checkUserInZone(Point(event.latitude, event.longitude), z.points)) {
print("USER IN ZONE - ${z.type}");
}
});
}
FutureOr<void> _onNotificationReceived(
NotificationReceived event,
Emitter<ScooterState> emit,
) {
print("NOTIFICATION RECEIVED: ${event.notification.content}");
emit(state.copyWith(lastNotification: event.notification));
}
bool checkUserInZone(Point userPos, List<Point> zonePoints) {
List<mt.LatLng> polygon = zonePoints
.map((p) => mt.LatLng(p.latitude, p.longitude))
.toList();
mt.LatLng userLatLng = mt.LatLng(userPos.latitude, userPos.longitude);
return mt.PolygonUtil.containsLocation(userLatLng, polygon, true);
}
FutureOr<void> _onFetchProfileData(
FetchProfileData event,
Emitter<ScooterState> emit,
) async {
try {
final profile = await getProfileUseCase();
emit(state.copyWith(phoneNumber: profile.phone, balance: profile.balance));
} catch (e) {
emit(
state.copyWith(
status: ScooterStatus.failure,
errorMessage: "FetchProfileData for SideMenu: ${e.toString()}",
),
);
}
}
FutureOr<void> _onLogoutPressed(
LogoutPressed event,
Emitter<ScooterState> emit,
) {
logoutUseCase();
splashBloc.add(AuthCheckRequested());
}
FutureOr<void> _onCheckUser(
CheckUser event,
Emitter<ScooterState> emit,
) async {
try {
final flags = await checkUserUseCase();
print("flags: $flags");
if (flags == null) {
return;
}
print("check user success");
emit(state.copyWith(flags: flags));
} catch (e) {
emit(
state.copyWith(
status: ScooterStatus.failure,
errorMessage: "CheckUser: ${e.toString()}",
),
);
}
}
}

View File

@@ -0,0 +1,90 @@
import 'dart:async';
import 'package:be_happy/domain/entities/map_settings.dart';
import 'package:be_happy/domain/usecase/get_map_settings_usecase.dart';
import 'package:be_happy/domain/usecase/get_pedestrian_routes_usecase.dart';
import 'package:be_happy/domain/usecase/get_scooter_usecase.dart';
import 'package:be_happy/domain/usecase/save_map_settings_usecase.dart';
import 'package:be_happy/presentation/event/map_settings_modal_event.dart';
import 'package:be_happy/presentation/state/map_settings_modal_state.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:yandex_mapkit/yandex_mapkit.dart';
class MapSettingsModalBloc extends Bloc<MapSettingsModalEvent, MapSettingsModalState> {
final GetMapSettingsUsecase getMapSettingsUsecase;
final SaveMapSettingsUsecase saveMapSettingsUsecase;
MapSettingsModalBloc(this.getMapSettingsUsecase, this.saveMapSettingsUsecase)
: super(MapSettingsModalState(
isAllGeomarksActive: true,
isAllGeozonesActive: true,
isParkingZoneActive: true,
isRestrictedParkingZoneActive: true,
isRestrictedDrivingZoneActive: true,
)) {
on<AllGeozonesToggled>(_onAllGeozonesToggled);
on<AllGeomarksToggled>(_onAllGeomarksToggled);
on<ParkingZonesToggled>(_onParkingZonesToggled);
on<RestrictedParkingZonesToggled>(_onRestrictedParkingZonesToggled);
on<RestrictedDrivingZonesToggled>(_onRestrictedDrivingZonesToggled);
on<ApllyButtonClick>(_onApplyClick);
on<MapSettingsModalStarted>(_onModalStarted);
}
FutureOr<void> _onAllGeozonesToggled(AllGeozonesToggled event, Emitter<MapSettingsModalState> emit) {
emit(state.copyWith(
isAllGeozonesActive: event.value,
isRestrictedParkingZoneActive: event.value,
isParkingZoneActive: event.value,
isRestrictedDrivingZoneActive: event.value,
));
}
FutureOr<void> _onParkingZonesToggled(ParkingZonesToggled event, Emitter<MapSettingsModalState> emit) {
final newState = state.copyWith(isParkingZoneActive: event.value);
emit(_calculateParentState(newState));
}
FutureOr<void> _onRestrictedParkingZonesToggled(RestrictedParkingZonesToggled event, Emitter<MapSettingsModalState> emit) {
final newState = state.copyWith(isRestrictedParkingZoneActive: event.value);
emit(_calculateParentState(newState));
}
FutureOr<void> _onRestrictedDrivingZonesToggled(RestrictedDrivingZonesToggled event, Emitter<MapSettingsModalState> emit) {
final newState = state.copyWith(isRestrictedDrivingZoneActive: event.value);
emit(_calculateParentState(newState));
}
MapSettingsModalState _calculateParentState(MapSettingsModalState currentState) {
final bool anyChildActive = currentState.isParkingZoneActive ||
currentState.isRestrictedParkingZoneActive ||
currentState.isRestrictedDrivingZoneActive;
return currentState.copyWith(isAllGeozonesActive: anyChildActive);
}
FutureOr<void> _onAllGeomarksToggled(AllGeomarksToggled event, Emitter<MapSettingsModalState> emit) {
emit(state.copyWith(isGeomarksActive: event.value));
}
FutureOr<void> _onApplyClick(ApllyButtonClick event, Emitter<MapSettingsModalState> emit) async {
MapSettings settings = MapSettings(
all_placemarks: state.isAllGeomarksActive,
all_zones: state.isAllGeozonesActive,
parking_zones: state.isParkingZoneActive,
restricted_parking_zones: state.isRestrictedParkingZoneActive,
restricted_driving_zones: state.isRestrictedDrivingZoneActive);
await saveMapSettingsUsecase(settings);
}
FutureOr<void> _onModalStarted(MapSettingsModalStarted event, Emitter<MapSettingsModalState> emit) async {
final settings = await getMapSettingsUsecase();
emit(state.copyWith(
isGeomarksActive: settings.all_placemarks,
isAllGeozonesActive: settings.all_zones,
isParkingZoneActive: settings.parking_zones,
isRestrictedParkingZoneActive: settings.restricted_parking_zones,
isRestrictedDrivingZoneActive: settings.restricted_driving_zones,
));
}
}

View File

@@ -0,0 +1,40 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'dart:developer' as dev;
import '../../../domain/repositories/news_repository.dart';
import '../event/news_event.dart';
import '../state/news_state.dart';
class NewsBloc extends Bloc<NewsEvent, NewsState> {
final NewsRepository _newsRepository;
NewsBloc(this._newsRepository) : super(const NewsState(status: NewsStatus.initial)) {
on<NewsFetchRequested>(_onFetchRequested);
}
Future<void> _onFetchRequested(
NewsFetchRequested event,
Emitter<NewsState> emit,
) async {
dev.log('NewsBloc: Получено событие NewsFetchRequested');
emit(state.copyWith(status: NewsStatus.loading));
try {
final news = await _newsRepository.getNews();
dev.log('NewsBloc: Успешно загружено ${news.length} новостей');
emit(state.copyWith(
status: NewsStatus.success,
news: news,
));
} catch (e, stackTrace) {
dev.log('NewsBloc: Ошибка: $e', stackTrace: stackTrace);
emit(state.copyWith(
status: NewsStatus.failure,
errorMessage: e.toString(),
));
}
}
}

View File

@@ -0,0 +1,107 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:be_happy/domain/entities/scooter_order.dart';
import 'package:be_happy/domain/usecase/get_scooter_order_history_usecase.dart';
import 'package:be_happy/core/result.dart';
// 🔹 EVENTS
abstract class OrderHistoryEvent {}
class OrderHistoryFetchRequested extends OrderHistoryEvent {
final int page;
OrderHistoryFetchRequested({this.page = 1});
}
class OrderHistoryRefreshRequested extends OrderHistoryEvent {}
// 🔹 STATES
enum OrderHistoryStatus { initial, loading, success, failure, empty }
class OrderHistoryState {
final OrderHistoryStatus status;
final List<ScooterOrder> orders;
final String? errorMessage;
final int currentPage;
final bool hasMore;
OrderHistoryState({
required this.status,
this.orders = const [],
this.errorMessage,
this.currentPage = 1,
this.hasMore = true,
});
OrderHistoryState copyWith({
OrderHistoryStatus? status,
List<ScooterOrder>? orders,
String? errorMessage,
int? currentPage,
bool? hasMore,
}) {
return OrderHistoryState(
status: status ?? this.status,
orders: orders ?? this.orders,
errorMessage: errorMessage ?? this.errorMessage,
currentPage: currentPage ?? this.currentPage,
hasMore: hasMore ?? this.hasMore,
);
}
}
class OrderHistoryBloc extends Bloc<OrderHistoryEvent, OrderHistoryState> {
final GetScooterOrderHistoryUsecase _usecase;
OrderHistoryBloc(this._usecase) : super(OrderHistoryState(status: OrderHistoryStatus.initial)) {
on<OrderHistoryFetchRequested>(_onFetchRequested);
on<OrderHistoryRefreshRequested>(_onRefreshRequested);
}
Future<void> _onFetchRequested(
OrderHistoryFetchRequested event,
Emitter<OrderHistoryState> emit,
) async {
if (event.page == 1) {
emit(state.copyWith(status: OrderHistoryStatus.loading));
}
final result = await _usecase.call(page: event.page);
// ✅ Явная проверка с правильным типом
if (result is Success<List<ScooterOrder>>) {
final orders = result.data;
if (orders == null || orders.isEmpty) {
emit(state.copyWith(
status: OrderHistoryStatus.empty,
orders: [],
));
} else {
final newOrders = event.page == 1
? orders
: [...state.orders, ...orders];
emit(state.copyWith(
status: OrderHistoryStatus.success,
orders: newOrders,
currentPage: event.page,
hasMore: orders.length == 20,
));
}
}
// ✅ Явно указываем тип Failure
else if (result is Failure<List<ScooterOrder>>) {
emit(state.copyWith(
status: OrderHistoryStatus.failure,
errorMessage: result.failure.message ?? 'Неизвестная ошибка',
));
}
}
Future<void> _onRefreshRequested(
OrderHistoryRefreshRequested event,
Emitter<OrderHistoryState> emit,
) async {
emit(state.copyWith(status: OrderHistoryStatus.loading));
add(OrderHistoryFetchRequested(page: 1));
}
}

View File

@@ -0,0 +1,124 @@
import 'dart:async';
import 'package:be_happy/domain/usecase/get_payment_cards_usecase.dart';
import 'package:be_happy/domain/usecase/get_profile_usecase.dart';
import 'package:be_happy/domain/usecase/get_scooter_order_by_id_usecase.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/result.dart';
import '../../../domain/entities/scooter_order.dart';
import '../../../domain/usecase/pay_ride_usecase.dart';
import '../../domain/entities/payment_card.dart';
import '../event/payment_confirm_event.dart';
import '../state/payment_confirm_state.dart';
class PaymentConfirmBloc
extends Bloc<PaymentConfirmEvent, PaymentConfirmState> {
final PayRideUsecase _payRideUsecase;
final GetScooterOrderByIdUsecase _getScooterOrderByIdUsecase;
final GetPaymentCardsUsecase _getPaymentCardsUsecase;
final GetProfileUseCase _getProfileUseCase;
PaymentConfirmBloc(
this._payRideUsecase,
this._getScooterOrderByIdUsecase,
this._getPaymentCardsUsecase,
this._getProfileUseCase,
) : super(const PaymentConfirmState()) {
on<PayRide>(_onPayRide);
on<PaymentConfirmStarted>(_onStarted);
on<PaymentCardChanged>(_onCardChanged);
on<SelectBalancePressed>(_onSelectBalancePressed);
}
Future<void> _onStarted(
PaymentConfirmStarted event,
Emitter<PaymentConfirmState> emit,
) async {
emit(state.copyWith(status: PaymentConfirmStatus.loading));
PaymentCard? mainCard;
final result = await _getScooterOrderByIdUsecase(event.orderId);
final cards_result = await _getPaymentCardsUsecase();
if (cards_result is Success<List<PaymentCard>>) {
mainCard = cards_result.data?.firstWhere(
(card) => card.isMain,
orElse: () => cards_result.data!.first,
);
}
switch (result) {
case Success<ScooterOrder>(data: final order):
emit(
state.copyWith(
status: PaymentConfirmStatus.initial,
order: order,
selectedCard: mainCard,
),
);
break;
case Failure<ScooterOrder>():
emit(
state.copyWith(
status: PaymentConfirmStatus.failure,
errorMessage: 'Не удалось загрузить данные поездки',
),
);
}
}
void _onCardChanged(
PaymentCardChanged event,
Emitter<PaymentConfirmState> emit,
) {
emit(state.copyWith(selectedCard: event.card, useBalance: false));
}
Future<void> _onPayRide(
PayRide event,
Emitter<PaymentConfirmState> emit,
) async {
emit(state.copyWith(status: PaymentConfirmStatus.loading));
final photoIds = event.photoIds.isEmpty ? [1] : event.photoIds;
final result = await _payRideUsecase(
event.orderId,
event.cardId,
event.isBalance,
);
if (result is Success<ScooterOrder>) {
emit(
state.copyWith(
status: PaymentConfirmStatus.success,
paymentCompleted: true,
),
);
} else if (result is Failure) {
emit(
state.copyWith(
status: PaymentConfirmStatus.failure,
errorMessage: 'Не удалось оплатить поездку',
),
);
}
}
FutureOr<void> _onSelectBalancePressed(
SelectBalancePressed event,
Emitter<PaymentConfirmState> emit,
) async {
final profile = await _getProfileUseCase();
emit(
state.copyWith(
useBalance: true,
userBalance: profile.balance,
selectedCard: null,
),
);
}
}

View File

@@ -0,0 +1,44 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../core/result.dart';
import '../../domain/entities/payment_card.dart';
import '../../domain/usecase/get_payment_cards_usecase.dart';
import '../event/payment_method_sheet_event.dart';
import '../state/payment_method_sheet_state.dart';
class PaymentMethodSheetBloc extends Bloc<PaymentMethodSheetEvent, PaymentMethodSheetState> {
final GetPaymentCardsUsecase _getPaymentCardsUsecase;
PaymentMethodSheetBloc(this._getPaymentCardsUsecase)
: super(PaymentMethodSheetState(status: PaymentMethodSheetStatus.initial)) {
on<PaymentMethodSheetStarted>(_onStarted);
}
Future<void> _onStarted(
PaymentMethodSheetStarted event,
Emitter<PaymentMethodSheetState> emit,
) async {
emit(state.copyWith(status: PaymentMethodSheetStatus.loading));
try {
final result = await _getPaymentCardsUsecase();
if (result is Success<List<PaymentCard>>) {
emit(state.copyWith(
status: PaymentMethodSheetStatus.success,
cards: result.data ?? [],
));
} else {
emit(state.copyWith(
status: PaymentMethodSheetStatus.failure,
errorMessage: 'Failed to load payment cards',
));
}
} catch (e) {
emit(state.copyWith(
status: PaymentMethodSheetStatus.failure,
errorMessage: e.toString(),
));
}
}
}

View File

@@ -0,0 +1,136 @@
import 'package:be_happy/domain/usecase/get_profile_usecase.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../core/result.dart';
import '../../core/failures.dart';
import '../../domain/entities/payment_card.dart';
import '../../domain/usecase/get_payment_cards_usecase.dart';
import '../../domain/usecase/remove_payment_card_usecase.dart';
import '../../domain/usecase/set_main_payment_card_usecase.dart';
import '../event/payment_methods_event.dart';
import '../state/payment_methods_state.dart';
class PaymentMethodsBloc extends Bloc<PaymentMethodsEvent, PaymentMethodsState> {
final GetPaymentCardsUsecase _getPaymentCardsUsecase;
final RemovePaymentCardUsecase _removePaymentCardUsecase;
final SetMainPaymentCardUsecase _setMainPaymentCardUsecase;
final GetProfileUseCase _getProfileUseCase;
PaymentMethodsBloc(
this._getPaymentCardsUsecase,
this._removePaymentCardUsecase,
this._setMainPaymentCardUsecase,
this._getProfileUseCase,
) : super(PaymentMethodsState(status: PaymentMethodsStatus.initial)) {
on<PaymentMethodsStarted>(_onStarted);
on<PaymentMethodsDeleteCard>(_onDeleteCard);
on<PaymentMethodsSetMainCard>(_onSetMainCard);
}
Future<void> _onStarted(
PaymentMethodsStarted event,
Emitter<PaymentMethodsState> emit,
) async {
emit(state.copyWith(status: PaymentMethodsStatus.loading));
try {
final result = await _getPaymentCardsUsecase();
final profile = await _getProfileUseCase();
if (result is Success<List<PaymentCard>>) {
emit(state.copyWith(
status: PaymentMethodsStatus.success,
cards: result.data ?? [],
balance: profile.balance,
));
} else if (result is Failure) {
String errorMessage = 'Не удалось загрузить карты';
/*if (result.failure is AuthFailure) {
errorMessage = 'Ошибка авторизации';
} else if (result.failure is UnknownFailure) {
errorMessage = result.failure.message ?? 'Неизвестная ошибка';
}*/
emit(state.copyWith(
status: PaymentMethodsStatus.failure,
errorMessage: errorMessage,
));
}
} catch (e) {
emit(state.copyWith(
status: PaymentMethodsStatus.failure,
errorMessage: e.toString(),
));
}
}
Future<void> _onDeleteCard(
PaymentMethodsDeleteCard event,
Emitter<PaymentMethodsState> emit,
) async {
emit(state.copyWith(isDeleting: true));
try {
final result = await _removePaymentCardUsecase(event.cardId);
if (result is Success) {
emit(state.copyWith(isDeleting: false));
add(PaymentMethodsStarted());
} else if (result is Failure) {
String errorMessage = 'Не удалось удалить карту';
if (result.failure is AuthFailure) {
errorMessage = 'Ошибка авторизации';
} else if (result.failure is UnknownFailure) {
errorMessage = result.failure.message ?? 'Неизвестная ошибка';
}
emit(state.copyWith(
isDeleting: false,
errorMessage: errorMessage,
));
}
} catch (e) {
emit(state.copyWith(
isDeleting: false,
errorMessage: e.toString(),
));
}
}
Future<void> _onSetMainCard(
PaymentMethodsSetMainCard event,
Emitter<PaymentMethodsState> emit,
) async {
emit(state.copyWith(isSettingMain: true));
try {
final result = await _setMainPaymentCardUsecase(event.cardId);
if (result is Success) {
emit(state.copyWith(isSettingMain: false));
add(PaymentMethodsStarted());
} else if (result is Failure) {
String errorMessage = 'Не удалось установить основную карту';
if (result.failure is AuthFailure) {
errorMessage = 'Ошибка авторизации';
} else if (result.failure is UnknownFailure) {
errorMessage = result.failure.message ?? 'Неизвестная ошибка';
}
emit(state.copyWith(
isSettingMain: false,
errorMessage: errorMessage,
));
}
} catch (e) {
emit(state.copyWith(
isSettingMain: false,
errorMessage: e.toString(),
));
}
}
}

View File

@@ -0,0 +1,84 @@
import 'dart:async';
import 'package:be_happy/domain/usecase/verify_pin_usecase.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../domain/usecase/is_pin_set_usecase.dart';
import '../event/pin_event.dart';
import '../state/pin_state.dart';
import '../../domain/usecase/create_pin_usecase.dart';
class PinBloc extends Bloc<PinEvent, PinState> {
final CreatePinUseCase createPinUseCase;
final VerifyPinUseCase verifyPinUsecase;
final IsPinSetUsecase isPinSetUsecase;
PinBloc({
required this.createPinUseCase,
required this.verifyPinUsecase,
required this.isPinSetUsecase,
bool isRegistration = true,
}) : super(const PinLoading()) {
on<PinDigitChanged>(_onPinChanged);
on<PinSubmitted>(_onPinSubmitted);
on<PinScreenStarted>(_onPinScreenStarted);
}
void _onPinChanged(
PinDigitChanged event,
Emitter<PinState> emit,
) {
if (state is PinCreateInProgress) {
emit(PinCreateInProgress(pin: event.pin));
} else if (state is PinLoginInProgress) {
emit(PinLoginInProgress(pin: event.pin));
}
}
Future<void> _onPinSubmitted(
PinSubmitted event,
Emitter<PinState> emit,
) async {
final currentPin = event.pin;
final bool isCreateMode = state is PinCreateInProgress;
emit(const PinLoading());
try {
if (isCreateMode) {
await createPinUseCase(currentPin);
emit(const PinSuccess());
} else {
final isValid = await verifyPinUsecase(currentPin);
if (isValid) {
emit(const PinSuccess());
} else {
throw Exception("Неверный ПИН-код");
}
}
} catch (e) {
if (isCreateMode) {
emit(PinCreateInProgress(pin: '', error: e.toString()));
} else {
emit(PinLoginInProgress(pin: '', error: e.toString().contains("Неверный")
? "Неверный ПИН-код"
: "Ошибка при проверке"));
}
}
}
FutureOr<void> _onPinScreenStarted(PinScreenStarted event, Emitter<PinState> emit) async {
try {
final bool hasPin = await isPinSetUsecase();
if (hasPin) {
// Пин ЕСТЬ в базе — значит, нужно его ВВЕСТИ (Login)
emit(const PinLoginInProgress(pin: ''));
} else {
// Пина НЕТ в базе — значит, нужно его СОЗДАТЬ (Create)
emit(const PinCreateInProgress(pin: ''));
}
} catch (e) {
emit(PinLoginInProgress(pin: '', error: "Ошибка доступа к хранилищу"));
}
}
}

View File

@@ -0,0 +1,75 @@
import 'package:be_happy/domain/usecase/upload_profile_photo_usecase.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../domain/entities/user_profile.dart';
import '../../domain/usecase/get_profile_usecase.dart';
import '../../domain/usecase/update_profile_usecase.dart';
import '../event/profile_event.dart';
import '../state/profile_state.dart';
class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
final GetProfileUseCase getProfileUseCase;
final UploadProfilePhotoUsecase uploadProfilePhotoUsecase;
final UpdateProfileUseCase updateProfileUseCase;
ProfileBloc(this.getProfileUseCase,
this.uploadProfilePhotoUsecase,
this.updateProfileUseCase)
: super(ProfileState.initial()) {
on<ProfileStarted>(_onStarted);
on<ProfileUpdated>(_onUpdated);
on<ProfilePhotoUpdated>(_onPhotoUploaded);
}
Future<void> _onStarted(
ProfileStarted event,
Emitter<ProfileState> emit,
) async {
emit(state.copyWith(isLoading: true));
print("_onStarted method was start");
try {
final profile = await getProfileUseCase();
print(profile.name);
emit(state.copyWith(isLoading: false, profile: profile));
} catch (e) {
emit(state.copyWith(isLoading: false, error: "STARTED: ${e.toString()}"));
}
}
Future<void> _onUpdated(
ProfileUpdated event,
Emitter<ProfileState> emit,
) async {
final profile = await getProfileUseCase();
emit(state.copyWith(profile: profile));
}
Future<void> _onPhotoUploaded(
ProfilePhotoUpdated event,
Emitter<ProfileState> emit,
) async {
emit(state.copyWith(isLoading: true));
try {
final avatarId = await uploadProfilePhotoUsecase(event.imageFile);
if (avatarId != null) {
// Отправляем PATCH запрос с новым avatarId
await updateProfileUseCase(UserProfile(
name: '',
birthDate: '',
phone: '',
email: '',
avatarId: avatarId,
));
}
final updatedProfile = await getProfileUseCase();
emit(state.copyWith(isLoading: false, profile: updatedProfile, error: null));
} catch (e) {
emit(state.copyWith(isLoading: false, error: "Ошибка загрузки: ${e.toString()}"));
}
}
}

View File

@@ -0,0 +1,63 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/result.dart';
import '../../../domain/entities/scooter_order.dart';
import '../../../domain/usecase/cancel_ride_usecase.dart';
import '../../../domain/usecase/start_ride_usecase.dart';
import '../event/reserved_ride_event.dart';
import '../state/reserved_ride_state.dart';
class ReservedRideBloc extends Bloc<ReservedRideEvent, ReservedRideState> {
final StartRideUsecase _startRideUsecase;
final CancelRideUsecase _cancelRideUsecase;
ReservedRideBloc(
this._startRideUsecase,
this._cancelRideUsecase,
) : super(const ReservedRideState()) {
on<StartRide>(_onStartRide);
on<CancelRide>(_onCancelRide);
}
Future<void> _onStartRide(
StartRide event,
Emitter<ReservedRideState> emit,
) async {
emit(state.copyWith(status: ReservedRideStatus.loading));
final result = await _startRideUsecase(event.orderId);
if (result is Success<ScooterOrder>) {
emit(state.copyWith(
status: ReservedRideStatus.success,
rideStarted: true,
));
} else if (result is Failure) {
emit(state.copyWith(
status: ReservedRideStatus.failure,
errorMessage: 'Не удалось начать поездку',
));
}
}
Future<void> _onCancelRide(
CancelRide event,
Emitter<ReservedRideState> emit,
) async {
emit(state.copyWith(status: ReservedRideStatus.loading));
final result = await _cancelRideUsecase(event.orderId);
if (result is Success<ScooterOrder>) {
emit(state.copyWith(
status: ReservedRideStatus.success,
rideCancelled: true,
));
} else if (result is Failure) {
emit(state.copyWith(
status: ReservedRideStatus.failure,
errorMessage: 'Не удалось отменить бронирование',
));
}
}
}

View File

@@ -0,0 +1,31 @@
import 'package:be_happy/core/result.dart';
import 'package:be_happy/domain/entities/point.dart';
import 'package:be_happy/domain/usecase/get_scooter_order_route_history_usecase.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../event/route_event.dart';
import '../state/route_state.dart';
class RouteBloc extends Bloc<RouteEvent, RouteState> {
final GetScooterOrderRouteHistoryUsecase getRouteUseCase;
RouteBloc({required this.getRouteUseCase}) : super(RouteInitial()) {
on<FetchRouteEvent>((event, emit) async {
emit(RouteLoading());
try {
final result = await getRouteUseCase(event.orderId);
if (result is Success<List<Point>>) {
final List<Point> points = result.data ?? [];
emit(RouteLoaded(points));
}
} catch (e) {
emit(RouteError("Ошибка загрузки пути"));
}
});
}
}

View File

@@ -0,0 +1,27 @@
import 'package:be_happy/domain/entities/scooter.dart';
import 'package:be_happy/domain/usecase/get_scooter_by_title_usecase.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../event/scooter_code_event.dart';
import '../state/scooter_code_state.dart';
class ScooterCodeBloc extends Bloc<ScooterCodeEvent, ScooterCodeState> {
final GetScooterByTitleUsecase getScooterByTitleUsecase;
ScooterCodeBloc({required this.getScooterByTitleUsecase}) : super(const ScooterCodeInitial()) {
on<ScooterCodeChanged>((event, emit) {
emit(ScooterCodeInitial(code: event.code));
});
on<ScooterCodeSubmitted>((event, emit) async {
emit(ScooterCodeLoading(code: event.code));
final result = await getScooterByTitleUsecase(event.code);
/*result.fold(
(error) => emit(ScooterCodeFailure(error.message ?? "Ошибка", code: event.code)),
(scooter) => emit(ScooterCodeSuccess(scooter, code: event.code)),
);*/
});
}
}

View File

@@ -0,0 +1,37 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../core/result.dart';
import '../../domain/entities/scooter.dart';
import '../../domain/usecase/get_scooter_usecase.dart';
import '../event/scooter_detail_event.dart';
import '../state/scooter_detail_state.dart';
class ScooterDetailBloc extends Bloc<ScooterDetailEvent, ScooterDetailState> {
final GetScooterUsecase _getScooterUsecase;
ScooterDetailBloc(this._getScooterUsecase) : super(const ScooterDetailState()) {
on<LoadScooterDetails>(_onLoadScooterDetails);
}
Future<void> _onLoadScooterDetails(
LoadScooterDetails event,
Emitter<ScooterDetailState> emit,
) async {
emit(state.copyWith(status: ScooterStatus.loading));
final result = await _getScooterUsecase(event.scooterId);
if (result is Success<Scooter?>) {
print("SCOOTER: ${result.data}");
emit(state.copyWith(
status: ScooterStatus.success,
scooter: result.data,
));
} else if (result is Failure) {
emit(state.copyWith(
status: ScooterStatus.failure,
errorMessage: "Не удалось загрузить данные",
));
}
}
}

View File

@@ -0,0 +1,91 @@
import 'package:be_happy/domain/usecase/get_pedestrian_routes_usecase.dart';
import 'package:be_happy/domain/usecase/get_scooter_usecase.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:yandex_mapkit/yandex_mapkit.dart';
import '../../core/result.dart';
import '../../domain/entities/scooter.dart';
import '../../domain/usecase/get_address_by_point_usecase.dart';
import '../event/scooter_detail_event.dart';
import '../event/scooter_detail_modal_event.dart';
import '../state/map_state.dart';
import '../state/scooter_detail_modal_state.dart';
class ScooterDetailModalBloc
extends Bloc<ScooterDetailModalEvent, ScooterDetailModalState> {
final GetAddressByPointUsecase _getAddressUsecase;
final GetPedestrianRoutesUsecase _getPedestrianRoutesUsecase;
final GetScooterUsecase _getScooterUsecase;
final Map<int, String> _addressCache = {};
final List<Scooter> fetch_scooters = [];
double? distance = 0.0;
ScooterDetailModalBloc(
this._getAddressUsecase,
this._getScooterUsecase,
this._getPedestrianRoutesUsecase,
) : super(
ScooterDetailModalState(
status: ScooterDetailModalStatus.initial,
),
) {
on<ScooterDetailModalStarted>(_onStarted);
}
Future<void> _onStarted(
ScooterDetailModalStarted event,
Emitter<ScooterDetailModalState> emit,
) async {
emit(state.copyWith(status: ScooterDetailModalStatus.loading));
final List<Scooter> updatedScooters = [];
String? firstAddress;
try {
for (var scooter in event.scooters) {
final result = await _getScooterUsecase(scooter.id);
String? currentAddress = _addressCache[scooter.id];
if (currentAddress == null) {
currentAddress = await _getAddressUsecase(scooter.longitude, scooter.latitude);
_addressCache[scooter.id] = currentAddress;
}
firstAddress ??= currentAddress;
final routes = await _getPedestrianRoutesUsecase(
Point(latitude: event.userLatitude, longitude: event.userLongitude),
Point(latitude: scooter.latitude, longitude: scooter.longitude),
);
final distance = routes?.firstOrNull?.metadata.weight.walkingDistance.value;
final time = routes?.firstOrNull?.metadata.weight.time.value;
if (result is Success<Scooter>) {
final data = result.data!;
data.distance = distance;
data.timeToTravel = time;
updatedScooters.add(data);
} else {
updatedScooters.add(scooter);
}
}
emit(
state.copyWith(
status: ScooterDetailModalStatus.success,
scooters: updatedScooters,
address: firstAddress ?? "Unknown address",
distance: updatedScooters.firstOrNull?.distance,
),
);
} catch (e) {
print('Error in Bloc: $e');
emit(state.copyWith(
status: ScooterDetailModalStatus.failure,
errorMessage: e.toString(),
));
}
}
}

View File

@@ -0,0 +1,78 @@
import 'dart:io';
import 'package:be_happy/domain/entities/scooter_order.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../domain/usecase/finish_ride_usecase.dart';
import '../../domain/usecase/upload_scooter_photos_usecase.dart';
import '../event/send_photo_event.dart';
import '../state/send_photo_state.dart';
import '../../core/result.dart';
import '../../core/failures.dart';
class SendPhotoBloc extends Bloc<SendPhotoEvent, SendPhotoState> {
final UploadScooterPhotosUsecase uploadScooterPhotosUsecase;
final FinishRideUsecase _finishRideUsecase;
SendPhotoBloc(this.uploadScooterPhotosUsecase, this._finishRideUsecase) : super(const SendPhotoState()) {
on<PhotoSelected>(_onPhotoSelected);
on<PhotoUploadSubmitted>(_onPhotoUploadSubmitted);
}
void _onPhotoSelected(PhotoSelected event, Emitter<SendPhotoState> emit) {
emit(state.copyWith(selectedImages: event.imagePaths));
}
Future<void> _onPhotoUploadSubmitted(
PhotoUploadSubmitted event, Emitter<SendPhotoState> emit) async {
if (state.selectedImages.isEmpty) {
emit(state.copyWith(
status: SendPhotoStatus.failure,
errorMessage: 'Выберите хотя бы одно фото',
));
return;
}
emit(state.copyWith(status: SendPhotoStatus.loading));
final imageFiles = state.selectedImages.map((path) => File(path)).toList();
final result = await uploadScooterPhotosUsecase(imageFiles);
switch (result) {
case Success<List<int>>():
// ✅ Защита: если data null или пустой — не вызываем finish
if (result.data == null || result.data!.isEmpty) {
emit(state.copyWith(
status: SendPhotoStatus.failure,
errorMessage: 'Не удалось получить ID загруженных фото',
));
return;
}
// ✅ Передаём два параметра в UseCase
final finishResult = await _finishRideUsecase(
event.orderId,
result.data! // ✅ ! т.к. проверили выше
);
switch (finishResult) {
case Success<ScooterOrder>():
emit(state.copyWith(
status: SendPhotoStatus.success,
recievedPhotoIds: result.data,
));
case Failure():
print("❌ FINISH ERROR: ${finishResult.failure.message}");
emit(state.copyWith(
status: SendPhotoStatus.failure,
errorMessage: 'Ошибка завершения поездки',
));
}
case Failure():
emit(state.copyWith(
status: SendPhotoStatus.failure,
errorMessage: 'Ошибка загрузки фото: ${result.failure.message}',
));
}
}
}

View File

@@ -0,0 +1,53 @@
import 'dart:async';
import 'package:be_happy/domain/usecase/refresh_token_usecase.dart';
import 'package:be_happy/presentation/event/spalsh_event.dart';
import 'package:be_happy/presentation/state/splash_state.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:shared_preferences/shared_preferences.dart';
class SplashBloc extends Bloc<SplashEvent, SplashState> {
final RefreshTokenUseCase _refreshTokenUseCase;
SplashBloc(this._refreshTokenUseCase):
super(AuthInitial()) {
on<AuthCheckRequested>(_onAuthCheckRequested);
on<AuthStarted>(_onAuthStarted);
on<PinVerificationSuccess>((event, emit) {
emit(AuthPinVerified());
});
}
Future<void> _onAuthCheckRequested(
AuthCheckRequested event,
Emitter<SplashState> emit,
) async {
print('AuthBloc: Получено событие AuthCheckRequested.');
final prefs = await SharedPreferences.getInstance();
final bool isFirstLaunch = prefs.getBool('is_first_launch') ?? true;
if (isFirstLaunch) {
await prefs.setBool('is_first_launch', false);
emit(AuthFirstLaunch());
return;
}
// 2. Если не первый запуск, пытаемся обновить токен
try {
print('AuthBloc: Пытаюсь обновить токен...');
// Здесь вызывается ваш метод refreshToken()
await _refreshTokenUseCase.execute();
print('AuthBloc: Токен успешно обновлен. Отправляю AuthAuthenticated.');
// Если метод выполнился без исключений, значит токен успешно обновлен
emit(AuthAuthenticated());
} catch (e) {
print('AuthBloc: Ошибка при обновлении токена: $e');
// Если refreshToken() бросил исключение, значит что-то пошло не так
emit(AuthUnauthenticated());
}
}
FutureOr<void> _onAuthStarted(AuthStarted event, Emitter<SplashState> emit) {
emit(AuthInProgress());
}
}

View File

@@ -0,0 +1,42 @@
import 'package:be_happy/core/result.dart';
import 'package:be_happy/domain/entities/subscription.dart';
import 'package:be_happy/domain/usecase/get_available_subscriptions_usecase.dart';
import 'package:be_happy/domain/usecase/get_client_subscriptions_usecase.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../event/subscription_list_event.dart';
import '../state/subscription_list_state.dart';
class SubscriptionListBloc extends Bloc<SubscriptionEvent, SubscriptionState> {
final GetAvailableSubscriptionsUsecase getAvailableSubscriptionsUsecase;
final GetClientSubscriptionsUsecase getClientSubscriptionsUsecase;
SubscriptionListBloc({
required this.getAvailableSubscriptionsUsecase,
required this.getClientSubscriptionsUsecase,
}) : super(SubscriptionsLoading()) {
on<LoadSubscriptionsEvent>((event, emit) async {
emit(SubscriptionsLoading());
try {
final results = await Future.wait([
getAvailableSubscriptionsUsecase(),
getClientSubscriptionsUsecase(),
]);
final allResult = results[0];
final activeResult = results[1] ;
if (allResult is Success<List<Subscription>> && activeResult is Success<List<Subscription>>) {
emit(SubscriptionsLoaded(
subscriptions: allResult.data ?? [],
activeSubscriptions: activeResult.data ?? [],
));
} else {
emit(SubscriptionsError("Не удалось загрузить данные из API"));
}
} catch (e) {
emit(SubscriptionsError(e.toString()));
}
});
}
}

View File

@@ -0,0 +1,66 @@
import 'package:be_happy/core/result.dart';
import 'package:be_happy/domain/entities/subscription.dart';
import 'package:be_happy/domain/usecase/activate_subscription_usecase.dart';
import 'package:be_happy/domain/usecase/get_subscription_by_id_usecase.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../event/subscription_details_event.dart';
import '../state/susbcription_details_state.dart';
class SubscriptionDetailsBloc extends Bloc<SubscriptionDetailsEvent, SubscriptionDetailsState> {
final GetSubscriptionByIdUsecase getSubscriptionByIdUsecase;
final ActivateSubscriptionUsecase activateSubscriptionUsecase;
SubscriptionDetailsBloc(this.getSubscriptionByIdUsecase,
this.activateSubscriptionUsecase) : super(DetailsLoading()) {
on<LoadDetailsEvent>((event, emit) async {
emit(DetailsLoading());
try {
final result = await getSubscriptionByIdUsecase(event.subscriptionId);
switch (result) {
case Success<Subscription>():
final sub = result.data;
if (sub == null) return;
emit(DetailsContentState(
subscription: sub,
selectedPeriod: sub.options.first,
));
case Failure<Subscription>():
emit(DetailsError("Ошибка при запросе данных"));
}
} catch (e) {
emit(DetailsError("Не удалось загрузить данные"));
}
});
on<SelectPeriodEvent>((event, emit) {
if (state is DetailsContentState) {
emit((state as DetailsContentState).copyWith(selectedPeriod: event.period));
}
});
on<ToggleAgreementEvent>((event, emit) {
if (state is DetailsContentState) {
emit((state as DetailsContentState).copyWith(isAgreed: event.value));
}
});
on<ActivateSubscriptionPressed>((event, emit) {
switch(state) {
case DetailsContentState contentState:
activateSubscriptionUsecase(contentState.selectedPeriod.id);
break;
default:
break;
}
});
}
}

View File

@@ -0,0 +1,128 @@
import 'dart:async';
import 'package:be_happy/domain/usecase/get_profile_usecase.dart';
import 'package:collection/collection.dart';
import 'package:be_happy/domain/entities/payment_card.dart';
import 'package:be_happy/domain/usecase/book_scooter_usecase.dart';
import 'package:be_happy/domain/usecase/get_payment_cards_usecase.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../core/result.dart';
import '../../domain/entities/tariff.dart';
import '../../domain/usecase/get_available_tariffs_usecase.dart';
import '../event/tariff_sheet_event.dart';
import '../state/tariff_sheet_state.dart';
class TariffSheetBloc extends Bloc<TariffSheetEvent, TariffSheetState> {
final GetAvailableTariffsUsecase _getAvailableTariffsUsecase;
final GetProfileUseCase _getProfileUseCase;
final GetPaymentCardsUsecase _getPaymentCardsUsecase;
final BookScooterUsecase _bookScooterUsecase;
TariffSheetBloc(
this._getAvailableTariffsUsecase,
this._getProfileUseCase,
this._getPaymentCardsUsecase,
this._bookScooterUsecase,
) : super(TariffSheetState(status: TariffSheetStatus.initial)) {
on<TariffSheetStarted>(_onStarted);
on<PaymentCardChanged>(_onPaymentCardChanged);
on<BookScooterPressed>(_onBookScooterPressed);
on<SelectBalancePressed>(_onSelectBalancePressed);
}
Future<void> _onStarted(
TariffSheetStarted event,
Emitter<TariffSheetState> emit,
) async {
emit(state.copyWith(status: TariffSheetStatus.loading));
try {
final result = await _getAvailableTariffsUsecase(event.scooterId);
final cards_result = await _getPaymentCardsUsecase();
if (result is Success<List<Tariff>> &&
cards_result is Success<List<PaymentCard>>) {
emit(
state.copyWith(
status: TariffSheetStatus.success,
tariffs: result.data ?? [],
selectedCard: cards_result.data?.firstWhereOrNull(
(element) => element.isMain,
),
),
);
} else {
emit(
state.copyWith(
status: TariffSheetStatus.failure,
errorMessage: 'Failed to load tariffs',
),
);
}
} catch (e, stackTrace) {
emit(
state.copyWith(
status: TariffSheetStatus.failure,
errorMessage: "ERROR: ${e.toString()} \n $stackTrace",
),
);
}
}
FutureOr<void> _onPaymentCardChanged(
PaymentCardChanged event,
Emitter<TariffSheetState> emit,
) {
try {
emit(
state.copyWith(
status: TariffSheetStatus.success,
selectedCard: event.card,
useBalance: false,
),
);
} catch (e) {
emit(
state.copyWith(
status: TariffSheetStatus.failure,
errorMessage: 'Failed to change card',
),
);
}
}
FutureOr<void> _onBookScooterPressed(
BookScooterPressed event,
Emitter<TariffSheetState> emit,
) {
try {
_bookScooterUsecase(
scooterId: event.scooterId,
planId: event.planId,
cardId: event.cardId,
subscriptionId: event.subscriptionId,
isBalance: event.isBalance,
isInsurance: event.isInsurance,
);
Future.delayed(const Duration(milliseconds: 300));
} catch (e) {
emit(
state.copyWith(
status: TariffSheetStatus.failure,
errorMessage: 'Failed to book scooter',
),
);
}
}
FutureOr<void> _onSelectBalancePressed(SelectBalancePressed event, Emitter<TariffSheetState> emit) async {
final profile = await _getProfileUseCase();
emit(state.copyWith(
useBalance: true,
userBalance: profile.balance,
selectedCard: null,
));
}
}

View File

@@ -0,0 +1,81 @@
import 'package:be_happy/domain/usecase/get_certificates_usecase.dart';
import 'package:be_happy/domain/usecase/get_payment_cards_usecase.dart';
import 'package:be_happy/domain/usecase/purchase_certificate_usecase.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../core/result.dart';
import '../../domain/entities/certificate.dart';
import '../../domain/entities/payment_card.dart';
import '../event/top_up_event.dart';
import '../state/top_up_state.dart';
class TopUpBloc extends Bloc<TopUpEvent, TopUpState> {
final GetCertificatesUsecase getCertificatesUsecase;
final PurchaseCertificateUsecase purchaseCertificateUsecase;
final GetPaymentCardsUsecase getUserCards;
TopUpBloc({
required this.getCertificatesUsecase,
required this.purchaseCertificateUsecase,
required this.getUserCards,
}) : super(TopUpState(isLoading: true)) {
on<LoadTopUpData>((event, emit) async {
emit(state.copyWith(isLoading: true));
// Запускаем оба запроса параллельно
final results = await Future.wait([
getCertificatesUsecase(),
getUserCards(),
]);
final certResult = results[0] as Result<List<Certificate>>;
final cardResult = results[1] as Result<List<PaymentCard>>;
switch ((certResult, cardResult)) {
case (Success(data: final certs), Success(data: final cards)):
emit(
state.copyWith(
certificates: certs,
cards: cards,
selectedTariff: certs!.length > 1
? certs[1]
: (certs.isNotEmpty ? certs.first : null),
selectedCard: cards!.isEmpty
? null
: cards.firstWhere(
(c) => c.isMain,
orElse: () => cards.first,
),
isLoading: false,
),
);
case (
Success<List<Certificate>>(data: null),
Failure<List<PaymentCard>>(),
):
// TODO: Handle this case.
throw UnimplementedError();
case (
Success<List<Certificate>>(data: [...]),
Failure<List<PaymentCard>>(),
):
// TODO: Handle this case.
throw UnimplementedError();
case (Failure<List<Certificate>>(), _):
// TODO: Handle this case.
throw UnimplementedError();
}
});
on<SelectCertificate>(
(event, emit) => emit(state.copyWith(selectedTariff: event.certificate)),
);
on<SelectCard>(
(event, emit) => emit(state.copyWith(selectedCard: event.card)),
);
on<ToggleAgreement>(
(event, emit) => emit(state.copyWith(isAgreed: event.value)),
);
}
}

View File

@@ -0,0 +1,92 @@
import 'dart:async';
import 'package:be_happy/core/failures.dart';
import 'package:be_happy/core/result.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../domain/usecase/verify_code_usecase.dart';
import '../event/verify_code_event.dart';
import '../state/verify_code_state.dart';
class VerifyCodeBloc extends Bloc<VerifyCodeEvent, VerifyCodeState> {
final VerifyCodeUseCase _verifyCodeUseCase;
Timer? _timer;
VerifyCodeBloc(this._verifyCodeUseCase)
: super(VerifyCodeState.initial()) {
on<VerifyCodeStarted>((event, emit) {
emit(state.copyWith(
phoneNumber: event.phoneNumber,
tempToken: event.tempToken,
secondsLeft: 60,
));
_startTimer();
});
on<CodeChanged>((event, emit) {
emit(state.copyWith(code: event.code));
});
on<ResendCodePressed>((event, emit) {
if (state.secondsLeft == 0) {
emit(state.copyWith(secondsLeft: 60));
_startTimer();
}
});
on<VerifyCodeSubmitted>(_onVerifyCodeSubmitted);
}
void _startTimer() {
_timer?.cancel();
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (state.secondsLeft <= 1) {
timer.cancel();
emit(state.copyWith(secondsLeft: 0));
} else {
emit(state.copyWith(secondsLeft: state.secondsLeft - 1));
}
});
}
Future<void> _onVerifyCodeSubmitted(VerifyCodeSubmitted event,
Emitter<VerifyCodeState> emit,) async {
if (state.isSubmitting || state.code.length != 6) return;
emit(state.copyWith(isSubmitting: true, error: null));
final result = await _verifyCodeUseCase.execute(state.code, state.tempToken);
final newState = switch (result) {
Success() => state.copyWith(
isSubmitting: false,
isSuccess: true,
),
Failure(failure: final f) => switch (f) {
AuthFailure(attemptsLeft: final count) => state.copyWith(
isSubmitting: false,
attemptsLeft: count,
error: 'Неверный код. Осталось попыток: $count',
),
AuthBlockFailure() => state.copyWith(
isSubmitting: false,
isBlocked: true,
error: 'Вы заблокированы за слишком частые попытки',
),
_ => state.copyWith(
isSubmitting: false,
error: 'Произошла ошибка. Попробуйте позже',
),
},
};
emit(newState);
}
@override
Future<void> close() {
_timer?.cancel();
return super.close();
}
}