bug fixes

This commit is contained in:
2026-09-04 13:41:57 +03:00
parent cf5b56e365
commit 5f57af2ad2
14 changed files with 201 additions and 89 deletions

View File

@@ -10,6 +10,6 @@ class AppSettingsService {
}
bool getMapSettingsFlag(String key) {
return sharedPreferences.getBool(key) ?? false;
return sharedPreferences.getBool(key) ?? true;
}
}

View File

@@ -6,10 +6,10 @@ class MapSettings {
final bool restricted_driving_zones;
MapSettings({
required this.all_placemarks,
required this.all_zones,
required this.parking_zones,
required this.restricted_parking_zones,
required this.restricted_driving_zones,
this.all_placemarks = true,
this.all_zones = true,
this.parking_zones = true,
this.restricted_parking_zones = true,
this.restricted_driving_zones = true,
});
}

View File

@@ -73,7 +73,6 @@ class MapSettingsSheet extends StatelessWidget {
TextButton(
onPressed: () {
context.read<MapSettingsModalBloc>().add(ApllyButtonClick());
context.read<MapBloc>().add(UpdateMap());
Navigator.of(context).pop();
},
child: const Text(

View File

@@ -3,8 +3,10 @@ import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../di/service_locator.dart';
import '../../event/map_event.dart';
import '../../event/reserved_ride_event.dart';
import '../../state/reserved_ride_state.dart';
import '../../viewmodel/map_bloc.dart';
import '../../viewmodel/reserved_ride_bloc.dart';
import '../dialog/cancel_booking_dialog.dart';
import '../gradient_button.dart';
@@ -125,7 +127,6 @@ class _ReservedRideSheetState extends State<ReservedRideSheet> {
const SizedBox(height: 20),
// ТАЙМЕР + ИНФО О САМОКАТЕ (КОМПАКТНЫЙ)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
@@ -227,6 +228,7 @@ class _ReservedRideSheetState extends State<ReservedRideSheet> {
listener: (context, state) {
if (state.rideStarted) {
Navigator.pop(context);
context.read<MapBloc>().add(FetchActiveOrders());
showModalBottomSheet(
context: context,
isScrollControlled: true,

View File

@@ -15,9 +15,11 @@ import '../../../domain/usecase/add_payment_card_usecase.dart';
import '../../../domain/usecase/get_payment_cards_usecase.dart';
import '../../../domain/usecase/get_profile_usecase.dart';
import '../../../domain/usecase/set_main_payment_card_usecase.dart';
import '../../event/map_event.dart';
import '../../event/payment_method_sheet_event.dart';
import '../../event/tariff_sheet_event.dart';
import '../../state/tariff_sheet_state.dart';
import '../../viewmodel/map_bloc.dart';
import '../../viewmodel/tariff_sheet_bloc.dart';
import '../gradient_button.dart';
import '../scooter/mini_battery_indicator.dart';
@@ -62,6 +64,14 @@ class _TariffSheetState extends State<TariffSheet> {
}
return BlocConsumer<TariffSheetBloc, TariffSheetState>(
listener: (context, state) {
if (state.status == TariffSheetStatus.success &&
state.tariffs.isNotEmpty &&
_selectedTariffIndex == null) {
setState(() {
_selectedTariffIndex = 0;
});
}
if (state.status == TariffSheetStatus.failure && state.errorMessage != null) {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
@@ -82,6 +92,8 @@ class _TariffSheetState extends State<TariffSheet> {
}
},
builder: (context, state) {
final selectedIndex = _selectedTariffIndex ?? (state.tariffs.isNotEmpty ? 0 : null);
if (state.status == TariffSheetStatus.loading) {
return Align(
alignment: Alignment.bottomCenter,
@@ -283,7 +295,7 @@ class _TariffSheetState extends State<TariffSheet> {
'Далее ${tariff.drivePrice.toStringAsFixed(2)} ${tariff.currency}/мин.',
'Минута на паузе ${tariff.pausePrice.toStringAsFixed(2)} ${tariff.currency}',
],
isSelected: _selectedTariffIndex == index,
isSelected: selectedIndex == index,
isInsurance: state.isInsurance,
tariff: tariff,
onTap: () {
@@ -412,19 +424,20 @@ class _TariffSheetState extends State<TariffSheet> {
height: 56,
width: double.infinity,
fontSize: 16,
enabled: _selectedTariffIndex != null && (state.selectedCard != null || state.useBalance),
onTap: (_selectedTariffIndex != null && (state.selectedCard != null || state.useBalance))
enabled: selectedIndex != null && (state.selectedCard != null || state.useBalance),
onTap: (selectedIndex != null && (state.selectedCard != null || state.useBalance))
? () {
context.read<TariffSheetBloc>().add(
BookScooterPressed(
widget.scooter.id,
state.tariffs[_selectedTariffIndex!].id,
state.tariffs[_selectedTariffIndex!].subscriptionId,
state.tariffs[selectedIndex].id,
state.tariffs[selectedIndex].subscriptionId,
state.useBalance ? null : state.selectedCard?.id,
state.useBalance,
state.isInsurance
)
);
context.read<MapBloc>().add(FetchActiveOrders());
context.pushReplacement('/home/current-rides-sheet');
}
: null,

View File

@@ -38,6 +38,8 @@ class FocusOnScooter extends ScooterEvent {
class ClearMapPlacemarks extends ScooterEvent {}
class ClearMapFocus extends ScooterEvent {}
class FetchActiveOrders extends ScooterEvent {}

View File

@@ -73,8 +73,8 @@ class _MapScreenState extends State<MapScreen> {
return current.lastNotification !=
previous.lastNotification ||
current.flags != previous.flags ||
previous.selectedScooterForFocus?.id
!= current.selectedScooterForFocus?.id;
previous.selectedScooterForFocus?.id !=
current.selectedScooterForFocus?.id;
},
listener: (context, state) {
@@ -176,7 +176,8 @@ class _MapScreenState extends State<MapScreen> {
final reservedScooters = _buildScooterPlacemarks(
scooters: state.reservedScooters ?? [],
iconAsset: 'assets/icons/scooter_reserved_placemark_fill.png',
iconAsset:
'assets/icons/scooter_reserved_placemark_fill.png',
isClickable: false,
);
@@ -368,6 +369,7 @@ class _MapScreenState extends State<MapScreen> {
}
});
}
Future<void> _moveCameraToPoint(
double lat,
double lon, {
@@ -401,17 +403,13 @@ class _MapScreenState extends State<MapScreen> {
}
if (flags.hasUnpaidOrder) {
_showExistingNotification(
child: UnpaidOrderNotificationCard(
onClose: () => BotToast.cleanAll(),
),
child: UnpaidOrderNotificationCard(onClose: () => BotToast.cleanAll()),
);
return;
}
if (flags.hasFine) {
_showExistingNotification(
child: FineNotificationCard(
onClose: () => BotToast.cleanAll(),
),
child: FineNotificationCard(onClose: () => BotToast.cleanAll()),
);
return;
}
@@ -567,7 +565,8 @@ class _MapScreenState extends State<MapScreen> {
}) {
return scooters.map((scooter) {
return PlacemarkMapObject(
mapId: MapObjectId('${isClickable ? "" : "reserved_"}${scooter.id}'), // уникальный ID для карты
mapId: MapObjectId('${isClickable ? "" : "reserved_"}${scooter.id}'),
// уникальный ID для карты
point: Point(latitude: scooter.longitude, longitude: scooter.latitude),
icon: PlacemarkIcon.single(
PlacemarkIconStyle(
@@ -595,7 +594,9 @@ class _MapScreenState extends State<MapScreen> {
for (var polygonData in zone.geometry) {
if (polygonData.isEmpty) continue;
var outerPoints = polygonData[0].map((p) => Point(latitude: p.latitude, longitude: p.longitude)).toList();
var outerPoints = polygonData[0]
.map((p) => Point(latitude: p.latitude, longitude: p.longitude))
.toList();
var cleanPoints = <Point>[];
for (var p in outerPoints) {
@@ -619,12 +620,14 @@ class _MapScreenState extends State<MapScreen> {
PolygonMapObject(
mapId: const MapObjectId('global_inverse_mask'),
polygon: Polygon(
outerRing: const LinearRing(points: [
outerRing: const LinearRing(
points: [
Point(latitude: 85, longitude: -179.9),
Point(latitude: 85, longitude: 179.9),
Point(latitude: -85, longitude: 179.9),
Point(latitude: -85, longitude: -179.9),
]),
],
),
innerRings: driveOuterRings, // Вырезаем только зоны Drive
),
strokeWidth: 0,
@@ -644,10 +647,14 @@ class _MapScreenState extends State<MapScreen> {
fillColor = Colors.transparent; // Прозрачная внутри маски
} else if (zone.type == "NotDrive") {
borderColor = const Color(0xFFEF4444); // Красный бордюр
fillColor = const Color(0xFFEF4444).withOpacity(0.3); // Красная полупрозрачная заливка для наглядности внутри Drive
fillColor = const Color(0xFFEF4444).withOpacity(
0.3,
); // Красная полупрозрачная заливка для наглядности внутри Drive
} else {
borderColor = const Color(0xFFA78BFA); // Фиолетовый бордюр (Парковка)
fillColor = const Color(0xFFA78BFA).withOpacity(0.25); // Фиолетовая заливка
fillColor = const Color(
0xFFA78BFA,
).withOpacity(0.25); // Фиолетовая заливка
}
int polyIndex = 0;
@@ -655,7 +662,9 @@ class _MapScreenState extends State<MapScreen> {
if (polygonData.isEmpty) continue;
// Строим внешнее кольцо текущего полигона
var outerPoints = polygonData[0].map((p) => Point(latitude: p.latitude, longitude: p.longitude)).toList();
var outerPoints = polygonData[0]
.map((p) => Point(latitude: p.latitude, longitude: p.longitude))
.toList();
if (outerPoints.first != outerPoints.last) {
outerPoints.add(outerPoints.first);
}
@@ -664,7 +673,9 @@ class _MapScreenState extends State<MapScreen> {
// Строим внутренние кольца (дыры), если они есть у этого полигона
List<LinearRing> innerRings = [];
for (int i = 1; i < polygonData.length; i++) {
var innerPoints = polygonData[i].map((p) => Point(latitude: p.latitude, longitude: p.longitude)).toList();
var innerPoints = polygonData[i]
.map((p) => Point(latitude: p.latitude, longitude: p.longitude))
.toList();
if (innerPoints.isNotEmpty) {
if (innerPoints.first != innerPoints.last) {
innerPoints.add(innerPoints.first);
@@ -677,10 +688,7 @@ class _MapScreenState extends State<MapScreen> {
objects.add(
PolygonMapObject(
mapId: MapObjectId('zone_poly_${zone.type}_${zone.id}_$polyIndex'),
polygon: Polygon(
outerRing: outerRing,
innerRings: innerRings,
),
polygon: Polygon(outerRing: outerRing, innerRings: innerRings),
strokeColor: borderColor,
strokeWidth: 2.0,
fillColor: fillColor,
@@ -721,10 +729,21 @@ class _MapScreenState extends State<MapScreen> {
onPressed: () => context.push("/home/notifications"),
),
const SizedBox(height: 12),
_RoundButton(
BlocBuilder<MapBloc, ScooterState>(
buildWhen: (previous, current) =>
previous.activeRidesCount != current.activeRidesCount,
builder: (context, state) {
return _RoundButton(
imagePath: 'assets/icons/scooter_placemark.png',
onPressed: () => context.push("/home/current-rides-sheet"),
)
count: state.activeRidesCount ?? 0,
onPressed: () {
context.push("/home/current-rides-sheet").then((_) {
context.read<MapBloc>().add(FetchActiveOrders());
});
},
);
},
),
],
),
),
@@ -765,9 +784,7 @@ class _MapScreenState extends State<MapScreen> {
}
if (flags.hasFine) {
_showExistingNotification(
child: FineNotificationCard(
onClose: () => BotToast.cleanAll(),
),
child: FineNotificationCard(onClose: () => BotToast.cleanAll()),
);
return;
}
@@ -870,23 +887,28 @@ class _RoundIconButton extends StatelessWidget {
class _RoundButton extends StatelessWidget {
final String imagePath;
final VoidCallback onPressed;
final int count;
const _RoundButton({
required this.imagePath,
required this.onPressed,
this.count = 0,
});
@override
Widget build(BuildContext context) {
return Container(
return GestureDetector(
onTap: onPressed,
child: Stack(
clipBehavior: Clip.none,
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColors.darkBlue,
borderRadius: BorderRadius.circular(12),
),
child: GestureDetector(
onTap: onPressed,
child: Center(
child: Image.asset(
imagePath,
@@ -897,6 +919,32 @@ class _RoundButton extends StatelessWidget {
),
),
),
if (count > 0)
Positioned(
top: -4,
right: -4,
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(
color: Colors.redAccent,
shape: BoxShape.circle,
),
constraints: const BoxConstraints(minWidth: 18, minHeight: 18),
child: Center(
child: Text(
'$count',
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
),
),
],
),
);
}
}
@@ -929,10 +977,12 @@ void _showExistingNotification({required Widget child}) {
duration: null, // не исчезает, пока пользователь не закроет
toastBuilder: (_) {
return Container(
margin: const EdgeInsets.only(top: 120), // тот же отступ, что в listener
margin: const EdgeInsets.only(top: 120),
// тот же отступ, что в listener
child: Material(
color: Colors.transparent,
child: child, // PaymentNotificationCard / UnpaidOrderNotificationCard / FineNotificationCard
child:
child, // PaymentNotificationCard / UnpaidOrderNotificationCard / FineNotificationCard
),
);
},

View File

@@ -30,13 +30,13 @@ class SupportScreen extends StatelessWidget {
),
const Divider(height: 1, color: Colors.white24),
const SizedBox(height: 12),
LinkRow(
/*LinkRow(
icon: 'assets/icons/whatsapp.png',
title: 'WhatsApp',
onTap: () => openLink('https://wa.me/375257110909'),
),
const Divider(height: 1, color: Colors.white24),
const SizedBox(height: 12),
const SizedBox(height: 12),*/
LinkRow(
icon: 'assets/icons/viber.png',
title: 'Viber',

View File

@@ -6,11 +6,11 @@ class MapSettingsModalState {
final bool isRestrictedParkingZoneActive;
MapSettingsModalState({
required this.isAllGeomarksActive,
required this.isAllGeozonesActive,
required this.isRestrictedDrivingZoneActive,
required this.isParkingZoneActive,
required this.isRestrictedParkingZoneActive,
this.isAllGeomarksActive = true,
this.isAllGeozonesActive = true,
this.isRestrictedDrivingZoneActive = true,
this.isParkingZoneActive = true,
this.isRestrictedParkingZoneActive = true,
});
MapSettingsModalState copyWith({

View File

@@ -22,6 +22,7 @@ class ScooterState {
final double balance;
final UserCheckFlags flags;
final ClientNotification? lastNotification;
final int activeRidesCount;
ScooterState({
this.scooters = const [],
@@ -38,6 +39,7 @@ class ScooterState {
this.balance = 999,
this.flags = const UserCheckFlags(hasFine: false, hasUnpaidOrder: false, hasCard: false),
this.lastNotification,
this.activeRidesCount = 0,
});
ScooterState copyWith({
@@ -55,6 +57,7 @@ class ScooterState {
double? balance,
UserCheckFlags? flags,
ClientNotification? lastNotification,
int? activeRidesCount,
}) {
return ScooterState(
scooters: scooters ?? this.scooters,
@@ -71,6 +74,7 @@ class ScooterState {
balance: balance ?? this.balance,
flags: flags ?? this.flags,
lastNotification: lastNotification ?? this.lastNotification,
activeRidesCount: activeRidesCount ?? this.activeRidesCount,
);
}
}

View File

@@ -17,7 +17,7 @@ class TariffSheetState {
this.status = TariffSheetStatus.initial,
this.tariffs = const [],
this.selectedCard,
this.isInsurance = false,
this.isInsurance = true,
this.errorMessage,
this.userBalance = 0,
this.useBalance = false,

View File

@@ -34,6 +34,7 @@ class MapBloc extends Bloc<ScooterEvent, ScooterState> {
final LogoutUseCase logoutUseCase;
final SplashBloc splashBloc;
StreamSubscription<ClientNotification>? _notificationSubscription;
Timer? _ordersTimer;
Future<List<Zone>>? _zonesFuture;
@@ -58,6 +59,7 @@ class MapBloc extends Bloc<ScooterEvent, ScooterState> {
on<FocusOnScooter>(_onFocusOnScooter);
on<ClearMapPlacemarks>(_onClearMapPlacemarks);
on<ClearMapFocus>(_onClearMapFocus);
on<FetchActiveOrders>(_onFetchActiveOrders);
}
Future<List<Zone>> _getZones() async {
@@ -344,4 +346,50 @@ class MapBloc extends Bloc<ScooterEvent, ScooterState> {
selectedScooterForFocus: null,
));
}
void _startOrdersTimer() {
_ordersTimer?.cancel();
// Опрашиваем раз в 15 секунд, если есть активные поездки
_ordersTimer = Timer.periodic(const Duration(seconds: 15), (_) {
add(FetchActiveOrders());
});
}
void _stopOrdersTimer() {
_ordersTimer?.cancel();
_ordersTimer = null;
}
Future<void> _onFetchActiveOrders(
FetchActiveOrders event,
Emitter<ScooterState> emit,
) async {
try {
final result = await getClientOrdersUsecase();
if (result is Success<List<ScooterOrder>>) {
final activeOrders = result.data ?? [];
final count = activeOrders.length;
// Управляем таймером в зависимости от наличия активных поездок
if (count > 0 && (_ordersTimer == null || !_ordersTimer!.isActive)) {
_startOrdersTimer();
} else if (count == 0) {
_stopOrdersTimer();
}
emit(state.copyWith(
activeRidesCount: count,
reservedScooters: activeOrders.map((e) => e.scooter).toList(),
));
}
} catch (e) {
print("Error fetching active orders: $e");
}
}
@override
Future<void> close() {
_stopOrdersTimer();
return super.close();
}
}

View File

@@ -15,13 +15,7 @@ class MapSettingsModalBloc extends Bloc<MapSettingsModalEvent, MapSettingsModalS
final SaveMapSettingsUsecase saveMapSettingsUsecase;
MapSettingsModalBloc(this.getMapSettingsUsecase, this.saveMapSettingsUsecase)
: super(MapSettingsModalState(
isAllGeomarksActive: true,
isAllGeozonesActive: true,
isParkingZoneActive: true,
isRestrictedParkingZoneActive: true,
isRestrictedDrivingZoneActive: true,
)) {
: super(MapSettingsModalState()) {
on<AllGeozonesToggled>(_onAllGeozonesToggled);
on<AllGeomarksToggled>(_onAllGeomarksToggled);
on<ParkingZonesToggled>(_onParkingZonesToggled);

View File

@@ -2,7 +2,7 @@ name: be_happy
description: ""
publish_to: 'none'
version: 1.0.3+1
version: 1.0.4+1
environment:
sdk: ^3.8.1